<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sebastian Maniak</title><link>https://maniak.io/</link><description>Practical guides for running AI agents in production.</description><language>en-us</language><atom:link href="https://maniak.io/index.xml" rel="self" type="application/rss+xml"/><item><title>One Front Door for Every Tool: MCP Multiplexing Through agentgateway</title><link>https://maniak.io/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/</guid><description>&lt;p>Here is a problem you don&amp;rsquo;t have with one MCP server, and can&amp;rsquo;t avoid with six.&lt;/p>
&lt;p>You wire up an MCP server for your firewall so an agent can read policies. Great. Then one for your load balancer. Then your network fabric, your cloud bill, your ticketing system. Each one is useful. But now every developer who wants the firewall tools in their IDE is pasting a URL and a credential into a config file. Multiply that by six systems and a dozen laptops and you&amp;rsquo;ve built a &lt;strong>credential sprawl machine&lt;/strong>: secrets for production infrastructure scattered across editors, each a place they can leak from, none of them centrally revocable.&lt;/p>
&lt;p>The fix isn&amp;rsquo;t fewer tools. It&amp;rsquo;s &lt;strong>one door&lt;/strong>. This article walks a real one running on &lt;a href="https://github.com/sebbycorp/k8s-viper">Viper&lt;/a> — a single &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> endpoint that multiplexes &lt;strong>seven&lt;/strong> MCP servers into one URL, so clients get every tool through one governed front door and never hold a device password at all.&lt;/p>
&lt;h2 id="the-sprawl-drawn">The sprawl, drawn&lt;/h2>
&lt;p>The thing we&amp;rsquo;re replacing looks like this — every client speaking every backend&amp;rsquo;s protocol, holding every backend&amp;rsquo;s secret:&lt;/p>
&lt;div class="mermaid">flowchart LR
 l1[&amp;#34;Laptop / IDE&amp;#34;]
 l2[&amp;#34;Laptop / IDE&amp;#34;]
 l3[&amp;#34;CI job&amp;#34;]
 f[&amp;#34;FortiGate&amp;#34;]
 b[&amp;#34;F5 BIG-IP&amp;#34;]
 a[&amp;#34;Arista cEOS&amp;#34;]
 aw[&amp;#34;AWS&amp;#34;]
 l1 --&amp;gt; f
 l1 --&amp;gt; b
 l1 --&amp;gt; a
 l2 --&amp;gt; f
 l2 --&amp;gt; aw
 l3 --&amp;gt; b
 l3 --&amp;gt; a
 style l1 fill:#FFF7D6,stroke:#17181C,color:#17181C
 style l2 fill:#FFF7D6,stroke:#17181C,color:#17181C
 style l3 fill:#FFF7D6,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Every one of those arrows is a place a credential lives. The picture we &lt;em>want&lt;/em> collapses all of that to a single hop into one proxy, with the backends hidden behind it:&lt;/p>
&lt;div class="mermaid">flowchart LR
 client[&amp;#34;Grok Bot / Cursor / Inspector&amp;#34;]
 gw[&amp;#34;agentgateway-proxy&amp;lt;br/&amp;gt;:30100 /mcp&amp;lt;br/&amp;gt;(viper-mcp backend)&amp;#34;]
 subgraph kagent[&amp;#34;in-cluster · ClusterIP only&amp;#34;]
 f[&amp;#34;fortigate-mcp&amp;#34;]
 b[&amp;#34;f5-bigip-mcp&amp;#34;]
 a[&amp;#34;arista-ceos-mcp&amp;#34;]
 aw[&amp;#34;aws-budget-mcp&amp;#34;]
 s[&amp;#34;servicenow-mcp&amp;#34;]
 g[&amp;#34;gcp-budget-mcp&amp;#34;]
 k[&amp;#34;kagent-tools&amp;#34;]
 end
 vault[&amp;#34;Vault&amp;#34;]
 client --&amp;gt;|&amp;#34;Streamable HTTP&amp;#34;| gw
 gw --&amp;gt; f &amp;amp; b &amp;amp; a &amp;amp; aw &amp;amp; s &amp;amp; g &amp;amp; k
 vault -.-&amp;gt;|&amp;#34;ExternalSecret&amp;#34;| kagent
 style client fill:#FFF7D6,stroke:#17181C,color:#17181C
 style gw fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style vault fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>One URL for the client. Seven backends it never sees. Every secret in Vault. That&amp;rsquo;s the whole idea — the rest is how it&amp;rsquo;s wired and why it&amp;rsquo;s more secure, not less.&lt;/p>
&lt;h2 id="the-catalog-behind-the-door">The catalog behind the door&lt;/h2>
&lt;p>The single endpoint is &lt;code>http://172.16.10.135:30100/mcp&lt;/code> — the same agentgateway that already fronts the OpenAI and Spark routes on this lab, just with one more backend attached. Behind it sit seven MCP servers, each a Deployment + ClusterIP in the &lt;code>kagent&lt;/code> namespace, each speaking Streamable HTTP on &lt;code>:8084/mcp&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool prefix&lt;/th>
&lt;th>MCP server&lt;/th>
&lt;th>Fronts&lt;/th>
&lt;th style="text-align:right">Tools&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>fortigate_&lt;/code>&lt;/td>
&lt;td>&lt;code>fortigate-mcp&lt;/code>&lt;/td>
&lt;td>FortiGate 80F firewall&lt;/td>
&lt;td style="text-align:right">22&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5-bigip_&lt;/code>&lt;/td>
&lt;td>&lt;code>f5-bigip-mcp&lt;/code>&lt;/td>
&lt;td>F5 BIG-IP&lt;/td>
&lt;td style="text-align:right">6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista-ceos_&lt;/code>&lt;/td>
&lt;td>&lt;code>arista-ceos-mcp&lt;/code>&lt;/td>
&lt;td>Arista cEOS fabric (eAPI)&lt;/td>
&lt;td style="text-align:right">6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws-budget_&lt;/code>&lt;/td>
&lt;td>&lt;code>aws-budget-mcp&lt;/code>&lt;/td>
&lt;td>AWS us-east-2 billing/capacity&lt;/td>
&lt;td style="text-align:right">11&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>servicenow_&lt;/code>&lt;/td>
&lt;td>&lt;code>servicenow-mcp&lt;/code>&lt;/td>
&lt;td>ServiceNow tickets&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp-budget_&lt;/code>&lt;/td>
&lt;td>&lt;code>gcp-budget-mcp&lt;/code>&lt;/td>
&lt;td>GCP us-east1 billing/capacity&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-tools_&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-tools&lt;/code>&lt;/td>
&lt;td>in-cluster Kubernetes reads&lt;/td>
&lt;td style="text-align:right">124&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A live &lt;code>tools/list&lt;/code> through the gateway on 2026-08-18 returned &lt;strong>185 tools&lt;/strong> across those seven targets. From the client&amp;rsquo;s side it&amp;rsquo;s one server; from the operator&amp;rsquo;s side it&amp;rsquo;s the entire lab. If those backend names look familiar, they should — several are the very &lt;a href="https://maniak.io/articles/2026-08-18-governed-sandbox-agents-kagent-arista/">sandbox agents&lt;/a> covered in earlier posts. The kagent &lt;code>SandboxAgent&lt;/code>s still talk to their MCP servers directly over ClusterIP; the gateway is an &lt;strong>additional&lt;/strong> front door for interactive MCP clients.&lt;/p>
&lt;h2 id="how-its-wired-one-backend-two-behaviors">How it&amp;rsquo;s wired: one backend, two behaviors&lt;/h2>
&lt;p>The multiplex is a single &lt;code>AgentgatewayBackend&lt;/code> named &lt;code>viper-mcp&lt;/code>. Here it is live on the cluster — accepted, with all seven targets:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backend-viper-mcp.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backend-viper-mcp.png" alt="Live kubectl dump of AgentgatewayBackend/viper-mcp: spec.mcp with failureMode FailOpen, prefixMode Conditional, and seven static StreamableHTTP targets (fortigate, f5-bigip, arista-ceos, aws-budget, servicenow, gcp-budget, kagent-tools) each on port 8084 path /mcp; status condition Accepted=True" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live &lt;code>kubectl&lt;/code> on k3s-viper, 2026-08-18 — the real CR, &lt;code>Accepted: True&lt;/code>. Seven &lt;code>static&lt;/code> targets, each &lt;code>*.kagent.svc.cluster.local:8084/mcp&lt;/code> over &lt;code>StreamableHTTP&lt;/code>.&lt;/em>&lt;/p>
&lt;p>Two fields in that spec do the interesting work:&lt;/p>
&lt;p>&lt;strong>&lt;code>failureMode: FailOpen&lt;/code>.&lt;/strong> When a client opens an MCP session, the gateway aggregates the tool lists of all seven backends. If one is down — say the FortiGate MCP pod is restarting — FailOpen means the session still comes up with the &lt;em>other six&lt;/em>, instead of the whole aggregate failing because one target was unreachable. One flaky backend degrades gracefully rather than taking down every tool.&lt;/p>
&lt;p>&lt;strong>&lt;code>prefixMode: Conditional&lt;/code>.&lt;/strong> Seven servers means name collisions are inevitable — more than one backend could plausibly expose a &lt;code>health&lt;/code> or a &lt;code>summary&lt;/code>. Conditional prefixing namespaces each tool by its target, so &lt;code>arista-ceos&lt;/code>&amp;rsquo;s BGP tool arrives at the client as &lt;code>arista-ceos_bgp_summary&lt;/code> and AWS&amp;rsquo;s as &lt;code>aws-budget_cost_month&lt;/code>. &amp;ldquo;Conditional&amp;rdquo; because the prefix is applied where it&amp;rsquo;s needed to disambiguate a large, multi-target catalog — which is exactly the situation with 185 tools.&lt;/p>
&lt;p>An &lt;code>HTTPRoute&lt;/code> named &lt;code>viper-mcp&lt;/code> attaches that backend to the &lt;code>agentgateway-proxy&lt;/code> Gateway on the path prefix &lt;code>/mcp&lt;/code>. You can see it living alongside the lab&amp;rsquo;s other routes:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backends-list.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backends-list.png" alt="Live kubectl list of AgentgatewayBackends and HTTPRoutes in agentgateway-system: desktop-api, desktop-novnc, dgx-spark-llm, openai, and viper-mcp all Accepted=True, with matching HTTPRoutes including viper-mcp aged 12m" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>The &lt;code>viper-mcp&lt;/code> backend and route sit next to the existing &lt;code>openai&lt;/code> and desktop routes — one Gateway, many front doors.&lt;/em>&lt;/p>
&lt;p>The whole thing is a handful of YAML in git (&lt;code>platform/agentgateway-ai/backend-viper-mcp.yaml&lt;/code> and &lt;code>httproute-viper-mcp.yaml&lt;/code>). No new gateway, no new ingress — just one more backend on a proxy that was already there. And it&amp;rsquo;s &lt;em>the same&lt;/em> proxy that fronts the models: &lt;code>/v1&lt;/code> (gpt-5.5), &lt;code>/spark&lt;/code> (Qwen), and &lt;code>/desktop&lt;/code> all live on this listener too. MCP is one more path on it, not a second box.&lt;/p>
&lt;h2 id="what-a-tool-call-actually-does">What a tool call actually does&lt;/h2>
&lt;p>The two behaviors above are easiest to understand by following a single request from &lt;code>initialize&lt;/code> to answer. Here&amp;rsquo;s a client asking for FortiGate policies:&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 autonumber
 participant C as Grok Bot / Cursor
 participant G as agentgateway&amp;lt;br/&amp;gt;:30100/mcp
 participant P as fortigate-mcp
 participant T as FortiGate 172.16.10.1

 C-&amp;gt;&amp;gt;G: initialize (Streamable HTTP)
 G--&amp;gt;&amp;gt;C: serverInfo — agentgateway 1.4.1
 C-&amp;gt;&amp;gt;G: tools/list
 G-&amp;gt;&amp;gt;P: list (and the other six, FailOpen)
 P--&amp;gt;&amp;gt;G: fg_list_policies …
 G--&amp;gt;&amp;gt;C: fortigate_fg_list_policies …
 C-&amp;gt;&amp;gt;G: tools/call fortigate_fg_list_policies
 G-&amp;gt;&amp;gt;P: strip prefix → fg_list_policies
 Note over P: Vault token is already in the pod
 P-&amp;gt;&amp;gt;T: FortiOS REST
 T--&amp;gt;&amp;gt;P: policies
 P--&amp;gt;&amp;gt;G: result
 G--&amp;gt;&amp;gt;C: result
&lt;/div>
&lt;p>Two moments on that diagram carry the whole design:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>On the way in (step 7), the prefix is stripped.&lt;/strong> The client calls &lt;code>fortigate_fg_list_policies&lt;/code>; the gateway routes on the &lt;code>fortigate_&lt;/code> prefix, removes it, and hands the backend the plain &lt;code>fg_list_policies&lt;/code> it actually implements. Namespacing is a gateway concern, invisible to the MCP server.&lt;/li>
&lt;li>&lt;strong>The credential never moves.&lt;/strong> The FortiOS token was synced into the &lt;code>fortigate-mcp&lt;/code> pod by ExternalSecret long before this request. The gateway doesn&amp;rsquo;t inject it, doesn&amp;rsquo;t see it, and the client never had it. The secret&amp;rsquo;s blast radius is one pod.&lt;/li>
&lt;/ul>
&lt;p>One more nuance worth stating: the kagent UI uses this &lt;em>same&lt;/em> Gateway for the &lt;strong>model&lt;/strong> (&lt;code>/v1&lt;/code> → gpt-5.5), but its SandboxAgents reach their tools over ClusterIP directly — they &lt;strong>skip&lt;/strong> the gateway for tool calls. The &lt;code>/mcp&lt;/code> front door exists for interactive clients like Grok Bot and Cursor, not for the in-cluster agents.&lt;/p>
&lt;h2 id="why-one-door-is-safer-than-many">Why one door is &lt;em>safer&lt;/em> than many&lt;/h2>
&lt;p>It&amp;rsquo;s tempting to read &amp;ldquo;central endpoint&amp;rdquo; as &amp;ldquo;central risk.&amp;rdquo; It&amp;rsquo;s the opposite, and the reasons are worth being explicit about:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The MCP servers never leave the cluster.&lt;/strong> Every backend is a ClusterIP Service. The only thing published on the node is the gateway&amp;rsquo;s &lt;code>:30100&lt;/code>, and it&amp;rsquo;s LAN-only. There is no path from the internet — or even from a random VLAN — to the FortiGate tools.&lt;/li>
&lt;li>&lt;strong>Device passwords and cloud keys stay in Vault.&lt;/strong> Each MCP server pulls its own credential from Vault via an ExternalSecret (&lt;code>secret/platform/fortigate&lt;/code>, &lt;code>.../f5-bigip&lt;/code>, and so on). The credential lives in the pod that needs it and nowhere else. A client calling through &lt;code>/mcp&lt;/code> never holds — never &lt;em>sees&lt;/em> — a device password.&lt;/li>
&lt;li>&lt;strong>One client identity instead of a dozen.&lt;/strong> Instead of every laptop authenticating to FortiOS, iControl, and eAPI in its own way, a single agentic client speaks to the gateway. One identity to reason about, one place to revoke.&lt;/li>
&lt;li>&lt;strong>The public surface stays documentation-only.&lt;/strong> The lab&amp;rsquo;s public site and GitHub Pages carry docs; the tool plane (&lt;code>:30100&lt;/code>) and the kagent UI (&lt;code>:30500&lt;/code>) are never published there.&lt;/li>
&lt;/ul>
&lt;p>Contrast that with the sprawl diagram: six protocols implemented in a dozen editors, each holding long-lived secrets. Centralizing the &lt;em>tool plane&lt;/em> behind one governed proxy shrinks the credential footprint from &amp;ldquo;everywhere&amp;rdquo; to &amp;ldquo;one namespace, backed by Vault.&amp;rdquo;&lt;/p>
&lt;h3 id="what-it-does--and-what-it-doesnt-yet">What it does — and what it doesn&amp;rsquo;t, yet&lt;/h3>
&lt;p>It&amp;rsquo;s worth being precise about which of these properties are &lt;em>wired today&lt;/em> versus which are the gateway&amp;rsquo;s potential. On this lab, right now, the gateway is doing exactly six jobs:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Job&lt;/th>
&lt;th>How it shows up here&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Single front door&lt;/td>
&lt;td>One &lt;code>agentgateway-proxy&lt;/code> for models, MCP, and desktop — not a second proxy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MCP multiplex&lt;/td>
&lt;td>Seven Streamable HTTP targets behind one &lt;code>/mcp&lt;/code> URL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Name isolation&lt;/td>
&lt;td>&lt;code>prefixMode: Conditional&lt;/code> → &lt;code>fortigate_fg_…&lt;/code>, &lt;code>arista-ceos_…&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Partial failure&lt;/td>
&lt;td>&lt;code>failureMode: FailOpen&lt;/code> — a dead budget MCP doesn&amp;rsquo;t hide FortiGate tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hide ClusterIP&lt;/td>
&lt;td>Clients never need &lt;code>fortigate-mcp.kagent:8084&lt;/code>, only &lt;code>:30100&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Keep secrets in Vault&lt;/td>
&lt;td>The gateway injects no device or cloud keys; the MCP pods already hold ExternalSecrets&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And, just as importantly, what is &lt;strong>not&lt;/strong> wired yet — so nobody mistakes this lab for a hardened deployment: there is &lt;strong>no client auth on &lt;code>/mcp&lt;/code>&lt;/strong> (it&amp;rsquo;s LAN, no bearer, same posture as &lt;code>/spark&lt;/code>), &lt;strong>no tool-level allow lists&lt;/strong>, and &lt;strong>no public-internet exposure&lt;/strong>. Anything on the LAN that can reach &lt;code>:30100&lt;/code> can call these tools. That&amp;rsquo;s an acceptable trade for a LAN lab; it would not be for production, and the honest move is to name it rather than imply a security boundary that isn&amp;rsquo;t there.&lt;/p>
&lt;h2 id="using-it-grok-bot-as-the-governed-client">Using it: Grok Bot as the governed client&lt;/h2>
&lt;p>The intended way to actually &lt;em>use&lt;/em> the multiplex is not to wire it into your editor — it&amp;rsquo;s to ask &lt;strong>Grok Bot&lt;/strong>, the k8s-viper agent, in plain language. It already has a jump onto Viper and calls &lt;code>http://127.0.0.1:30100/mcp&lt;/code> from the host, so you never paste a token and never publish &lt;code>/mcp&lt;/code>:&lt;/p>
&lt;blockquote>
&lt;ul>
&lt;li>&amp;ldquo;What is the BGP summary on spine1?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;List FortiGate policies that mention YouTube.&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;AWS MTD spend in us-east-2.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>One sentence in; the agent picks the right namespaced tool (&lt;code>arista-ceos_bgp_summary&lt;/code>, &lt;code>fortigate_*&lt;/code>, &lt;code>aws-budget_cost_month&lt;/code>), calls it through the gateway, and answers. You&amp;rsquo;re driving seven systems&amp;rsquo; worth of read-only tooling through a single chat, and the credentials for all of them stayed in Vault the entire time.&lt;/p>
&lt;p>If you &lt;em>do&lt;/em> want raw tools in a client, the same URL works from anything on the LAN — a Cursor &lt;code>mcpServers&lt;/code> entry pointed at &lt;code>http://172.16.10.135:30100/mcp&lt;/code>, or MCP Inspector over Streamable HTTP — with the standing rule that it never goes on a public machine. (A quick sanity check: &lt;code>GET /&lt;/code> on &lt;code>:30100&lt;/code> returns &lt;code>404 route not found&lt;/code>. That&amp;rsquo;s expected — the tools live at &lt;code>/mcp&lt;/code>, not the root.)&lt;/p>
&lt;h2 id="the-rules-that-keep-it-honest">The rules that keep it honest&lt;/h2>
&lt;p>The doc this is drawn from is blunt about its own guardrails, and they&amp;rsquo;re worth repeating because they&amp;rsquo;re what make &amp;ldquo;one door&amp;rdquo; defensible:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LAN only.&lt;/strong> Do not publish &lt;code>:30100&lt;/code> or the kagent UI &lt;code>:30500&lt;/code> on the public site.&lt;/li>
&lt;li>&lt;strong>Vault path names only in git.&lt;/strong> Never commit a secret value; never paste one into chat.&lt;/li>
&lt;li>&lt;strong>Don&amp;rsquo;t bump the pins to &amp;ldquo;fix&amp;rdquo; a missing tool.&lt;/strong> kagent &lt;code>0.10.0-rc2&lt;/code> and Substrate &lt;code>0.0.9&lt;/code> are pinned on purpose.&lt;/li>
&lt;li>&lt;strong>FailOpen is a feature, not a mask.&lt;/strong> A missing tool usually means a backend pod is down — check the Deployment, don&amp;rsquo;t assume the gateway is broken.&lt;/li>
&lt;/ul>
&lt;h2 id="the-takeaway">The takeaway&lt;/h2>
&lt;p>MCP is going to give every system in your environment an agent-friendly interface. That&amp;rsquo;s the good news and the scaling problem in one sentence: the moment you have several, &amp;ldquo;how does each client reach each server, and who holds the keys?&amp;rdquo; becomes the real design question.&lt;/p>
&lt;p>Multiplexing answers it by inverting the topology. Instead of &lt;em>N clients × M servers&lt;/em> each carrying credentials, you get &lt;strong>one front door&lt;/strong>, one client identity, and M servers that never leave the cluster with their secrets sealed in Vault. The client experience gets simpler — one URL, every tool — and the security posture gets &lt;em>tighter&lt;/em>, not looser. On Viper that&amp;rsquo;s 185 tools across seven systems, reachable by asking Grok Bot a question in English, with not a single device password ever leaving the cluster.&lt;/p>
&lt;p>One door. Every tool. Keys stay home.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The multiplex config, the full server catalog, and the live captures in this article are documented in &lt;a href="https://github.com/sebbycorp/k8s-viper/tree/main/docs/mcp-servers">k8s-viper / docs/mcp-servers&lt;/a>, with the individual MCP servers in &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos">sebbycorp/kagent-agent-substrate-demos&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>Here is a problem you don&amp;rsquo;t have with one MCP server, and can&amp;rsquo;t avoid with six.&lt;/p>
&lt;p>You wire up an MCP server for your firewall so an agent can read policies. Great. Then one for your load balancer. Then your network fabric, your cloud bill, your ticketing system. Each one is useful. But now every developer who wants the firewall tools in their IDE is pasting a URL and a credential into a config file. Multiply that by six systems and a dozen laptops and you&amp;rsquo;ve built a &lt;strong>credential sprawl machine&lt;/strong>: secrets for production infrastructure scattered across editors, each a place they can leak from, none of them centrally revocable.&lt;/p>
&lt;p>The fix isn&amp;rsquo;t fewer tools. It&amp;rsquo;s &lt;strong>one door&lt;/strong>. This article walks a real one running on &lt;a href="https://github.com/sebbycorp/k8s-viper">Viper&lt;/a> — a single &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> endpoint that multiplexes &lt;strong>seven&lt;/strong> MCP servers into one URL, so clients get every tool through one governed front door and never hold a device password at all.&lt;/p>
&lt;h2 id="the-sprawl-drawn">The sprawl, drawn&lt;/h2>
&lt;p>The thing we&amp;rsquo;re replacing looks like this — every client speaking every backend&amp;rsquo;s protocol, holding every backend&amp;rsquo;s secret:&lt;/p>
&lt;div class="mermaid">flowchart LR
 l1[&amp;#34;Laptop / IDE&amp;#34;]
 l2[&amp;#34;Laptop / IDE&amp;#34;]
 l3[&amp;#34;CI job&amp;#34;]
 f[&amp;#34;FortiGate&amp;#34;]
 b[&amp;#34;F5 BIG-IP&amp;#34;]
 a[&amp;#34;Arista cEOS&amp;#34;]
 aw[&amp;#34;AWS&amp;#34;]
 l1 --&amp;gt; f
 l1 --&amp;gt; b
 l1 --&amp;gt; a
 l2 --&amp;gt; f
 l2 --&amp;gt; aw
 l3 --&amp;gt; b
 l3 --&amp;gt; a
 style l1 fill:#FFF7D6,stroke:#17181C,color:#17181C
 style l2 fill:#FFF7D6,stroke:#17181C,color:#17181C
 style l3 fill:#FFF7D6,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Every one of those arrows is a place a credential lives. The picture we &lt;em>want&lt;/em> collapses all of that to a single hop into one proxy, with the backends hidden behind it:&lt;/p>
&lt;div class="mermaid">flowchart LR
 client[&amp;#34;Grok Bot / Cursor / Inspector&amp;#34;]
 gw[&amp;#34;agentgateway-proxy&amp;lt;br/&amp;gt;:30100 /mcp&amp;lt;br/&amp;gt;(viper-mcp backend)&amp;#34;]
 subgraph kagent[&amp;#34;in-cluster · ClusterIP only&amp;#34;]
 f[&amp;#34;fortigate-mcp&amp;#34;]
 b[&amp;#34;f5-bigip-mcp&amp;#34;]
 a[&amp;#34;arista-ceos-mcp&amp;#34;]
 aw[&amp;#34;aws-budget-mcp&amp;#34;]
 s[&amp;#34;servicenow-mcp&amp;#34;]
 g[&amp;#34;gcp-budget-mcp&amp;#34;]
 k[&amp;#34;kagent-tools&amp;#34;]
 end
 vault[&amp;#34;Vault&amp;#34;]
 client --&amp;gt;|&amp;#34;Streamable HTTP&amp;#34;| gw
 gw --&amp;gt; f &amp;amp; b &amp;amp; a &amp;amp; aw &amp;amp; s &amp;amp; g &amp;amp; k
 vault -.-&amp;gt;|&amp;#34;ExternalSecret&amp;#34;| kagent
 style client fill:#FFF7D6,stroke:#17181C,color:#17181C
 style gw fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style vault fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>One URL for the client. Seven backends it never sees. Every secret in Vault. That&amp;rsquo;s the whole idea — the rest is how it&amp;rsquo;s wired and why it&amp;rsquo;s more secure, not less.&lt;/p>
&lt;h2 id="the-catalog-behind-the-door">The catalog behind the door&lt;/h2>
&lt;p>The single endpoint is &lt;code>http://172.16.10.135:30100/mcp&lt;/code> — the same agentgateway that already fronts the OpenAI and Spark routes on this lab, just with one more backend attached. Behind it sit seven MCP servers, each a Deployment + ClusterIP in the &lt;code>kagent&lt;/code> namespace, each speaking Streamable HTTP on &lt;code>:8084/mcp&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool prefix&lt;/th>
&lt;th>MCP server&lt;/th>
&lt;th>Fronts&lt;/th>
&lt;th style="text-align:right">Tools&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>fortigate_&lt;/code>&lt;/td>
&lt;td>&lt;code>fortigate-mcp&lt;/code>&lt;/td>
&lt;td>FortiGate 80F firewall&lt;/td>
&lt;td style="text-align:right">22&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5-bigip_&lt;/code>&lt;/td>
&lt;td>&lt;code>f5-bigip-mcp&lt;/code>&lt;/td>
&lt;td>F5 BIG-IP&lt;/td>
&lt;td style="text-align:right">6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista-ceos_&lt;/code>&lt;/td>
&lt;td>&lt;code>arista-ceos-mcp&lt;/code>&lt;/td>
&lt;td>Arista cEOS fabric (eAPI)&lt;/td>
&lt;td style="text-align:right">6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws-budget_&lt;/code>&lt;/td>
&lt;td>&lt;code>aws-budget-mcp&lt;/code>&lt;/td>
&lt;td>AWS us-east-2 billing/capacity&lt;/td>
&lt;td style="text-align:right">11&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>servicenow_&lt;/code>&lt;/td>
&lt;td>&lt;code>servicenow-mcp&lt;/code>&lt;/td>
&lt;td>ServiceNow tickets&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp-budget_&lt;/code>&lt;/td>
&lt;td>&lt;code>gcp-budget-mcp&lt;/code>&lt;/td>
&lt;td>GCP us-east1 billing/capacity&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-tools_&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-tools&lt;/code>&lt;/td>
&lt;td>in-cluster Kubernetes reads&lt;/td>
&lt;td style="text-align:right">124&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A live &lt;code>tools/list&lt;/code> through the gateway on 2026-08-18 returned &lt;strong>185 tools&lt;/strong> across those seven targets. From the client&amp;rsquo;s side it&amp;rsquo;s one server; from the operator&amp;rsquo;s side it&amp;rsquo;s the entire lab. If those backend names look familiar, they should — several are the very &lt;a href="https://maniak.io/articles/2026-08-18-governed-sandbox-agents-kagent-arista/">sandbox agents&lt;/a> covered in earlier posts. The kagent &lt;code>SandboxAgent&lt;/code>s still talk to their MCP servers directly over ClusterIP; the gateway is an &lt;strong>additional&lt;/strong> front door for interactive MCP clients.&lt;/p>
&lt;h2 id="how-its-wired-one-backend-two-behaviors">How it&amp;rsquo;s wired: one backend, two behaviors&lt;/h2>
&lt;p>The multiplex is a single &lt;code>AgentgatewayBackend&lt;/code> named &lt;code>viper-mcp&lt;/code>. Here it is live on the cluster — accepted, with all seven targets:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backend-viper-mcp.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backend-viper-mcp.png" alt="Live kubectl dump of AgentgatewayBackend/viper-mcp: spec.mcp with failureMode FailOpen, prefixMode Conditional, and seven static StreamableHTTP targets (fortigate, f5-bigip, arista-ceos, aws-budget, servicenow, gcp-budget, kagent-tools) each on port 8084 path /mcp; status condition Accepted=True" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live &lt;code>kubectl&lt;/code> on k3s-viper, 2026-08-18 — the real CR, &lt;code>Accepted: True&lt;/code>. Seven &lt;code>static&lt;/code> targets, each &lt;code>*.kagent.svc.cluster.local:8084/mcp&lt;/code> over &lt;code>StreamableHTTP&lt;/code>.&lt;/em>&lt;/p>
&lt;p>Two fields in that spec do the interesting work:&lt;/p>
&lt;p>&lt;strong>&lt;code>failureMode: FailOpen&lt;/code>.&lt;/strong> When a client opens an MCP session, the gateway aggregates the tool lists of all seven backends. If one is down — say the FortiGate MCP pod is restarting — FailOpen means the session still comes up with the &lt;em>other six&lt;/em>, instead of the whole aggregate failing because one target was unreachable. One flaky backend degrades gracefully rather than taking down every tool.&lt;/p>
&lt;p>&lt;strong>&lt;code>prefixMode: Conditional&lt;/code>.&lt;/strong> Seven servers means name collisions are inevitable — more than one backend could plausibly expose a &lt;code>health&lt;/code> or a &lt;code>summary&lt;/code>. Conditional prefixing namespaces each tool by its target, so &lt;code>arista-ceos&lt;/code>&amp;rsquo;s BGP tool arrives at the client as &lt;code>arista-ceos_bgp_summary&lt;/code> and AWS&amp;rsquo;s as &lt;code>aws-budget_cost_month&lt;/code>. &amp;ldquo;Conditional&amp;rdquo; because the prefix is applied where it&amp;rsquo;s needed to disambiguate a large, multi-target catalog — which is exactly the situation with 185 tools.&lt;/p>
&lt;p>An &lt;code>HTTPRoute&lt;/code> named &lt;code>viper-mcp&lt;/code> attaches that backend to the &lt;code>agentgateway-proxy&lt;/code> Gateway on the path prefix &lt;code>/mcp&lt;/code>. You can see it living alongside the lab&amp;rsquo;s other routes:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backends-list.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-viper-mcp-multiplex-agentgateway-front-door/backends-list.png" alt="Live kubectl list of AgentgatewayBackends and HTTPRoutes in agentgateway-system: desktop-api, desktop-novnc, dgx-spark-llm, openai, and viper-mcp all Accepted=True, with matching HTTPRoutes including viper-mcp aged 12m" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>The &lt;code>viper-mcp&lt;/code> backend and route sit next to the existing &lt;code>openai&lt;/code> and desktop routes — one Gateway, many front doors.&lt;/em>&lt;/p>
&lt;p>The whole thing is a handful of YAML in git (&lt;code>platform/agentgateway-ai/backend-viper-mcp.yaml&lt;/code> and &lt;code>httproute-viper-mcp.yaml&lt;/code>). No new gateway, no new ingress — just one more backend on a proxy that was already there. And it&amp;rsquo;s &lt;em>the same&lt;/em> proxy that fronts the models: &lt;code>/v1&lt;/code> (gpt-5.5), &lt;code>/spark&lt;/code> (Qwen), and &lt;code>/desktop&lt;/code> all live on this listener too. MCP is one more path on it, not a second box.&lt;/p>
&lt;h2 id="what-a-tool-call-actually-does">What a tool call actually does&lt;/h2>
&lt;p>The two behaviors above are easiest to understand by following a single request from &lt;code>initialize&lt;/code> to answer. Here&amp;rsquo;s a client asking for FortiGate policies:&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 autonumber
 participant C as Grok Bot / Cursor
 participant G as agentgateway&amp;lt;br/&amp;gt;:30100/mcp
 participant P as fortigate-mcp
 participant T as FortiGate 172.16.10.1

 C-&amp;gt;&amp;gt;G: initialize (Streamable HTTP)
 G--&amp;gt;&amp;gt;C: serverInfo — agentgateway 1.4.1
 C-&amp;gt;&amp;gt;G: tools/list
 G-&amp;gt;&amp;gt;P: list (and the other six, FailOpen)
 P--&amp;gt;&amp;gt;G: fg_list_policies …
 G--&amp;gt;&amp;gt;C: fortigate_fg_list_policies …
 C-&amp;gt;&amp;gt;G: tools/call fortigate_fg_list_policies
 G-&amp;gt;&amp;gt;P: strip prefix → fg_list_policies
 Note over P: Vault token is already in the pod
 P-&amp;gt;&amp;gt;T: FortiOS REST
 T--&amp;gt;&amp;gt;P: policies
 P--&amp;gt;&amp;gt;G: result
 G--&amp;gt;&amp;gt;C: result
&lt;/div>
&lt;p>Two moments on that diagram carry the whole design:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>On the way in (step 7), the prefix is stripped.&lt;/strong> The client calls &lt;code>fortigate_fg_list_policies&lt;/code>; the gateway routes on the &lt;code>fortigate_&lt;/code> prefix, removes it, and hands the backend the plain &lt;code>fg_list_policies&lt;/code> it actually implements. Namespacing is a gateway concern, invisible to the MCP server.&lt;/li>
&lt;li>&lt;strong>The credential never moves.&lt;/strong> The FortiOS token was synced into the &lt;code>fortigate-mcp&lt;/code> pod by ExternalSecret long before this request. The gateway doesn&amp;rsquo;t inject it, doesn&amp;rsquo;t see it, and the client never had it. The secret&amp;rsquo;s blast radius is one pod.&lt;/li>
&lt;/ul>
&lt;p>One more nuance worth stating: the kagent UI uses this &lt;em>same&lt;/em> Gateway for the &lt;strong>model&lt;/strong> (&lt;code>/v1&lt;/code> → gpt-5.5), but its SandboxAgents reach their tools over ClusterIP directly — they &lt;strong>skip&lt;/strong> the gateway for tool calls. The &lt;code>/mcp&lt;/code> front door exists for interactive clients like Grok Bot and Cursor, not for the in-cluster agents.&lt;/p>
&lt;h2 id="why-one-door-is-safer-than-many">Why one door is &lt;em>safer&lt;/em> than many&lt;/h2>
&lt;p>It&amp;rsquo;s tempting to read &amp;ldquo;central endpoint&amp;rdquo; as &amp;ldquo;central risk.&amp;rdquo; It&amp;rsquo;s the opposite, and the reasons are worth being explicit about:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The MCP servers never leave the cluster.&lt;/strong> Every backend is a ClusterIP Service. The only thing published on the node is the gateway&amp;rsquo;s &lt;code>:30100&lt;/code>, and it&amp;rsquo;s LAN-only. There is no path from the internet — or even from a random VLAN — to the FortiGate tools.&lt;/li>
&lt;li>&lt;strong>Device passwords and cloud keys stay in Vault.&lt;/strong> Each MCP server pulls its own credential from Vault via an ExternalSecret (&lt;code>secret/platform/fortigate&lt;/code>, &lt;code>.../f5-bigip&lt;/code>, and so on). The credential lives in the pod that needs it and nowhere else. A client calling through &lt;code>/mcp&lt;/code> never holds — never &lt;em>sees&lt;/em> — a device password.&lt;/li>
&lt;li>&lt;strong>One client identity instead of a dozen.&lt;/strong> Instead of every laptop authenticating to FortiOS, iControl, and eAPI in its own way, a single agentic client speaks to the gateway. One identity to reason about, one place to revoke.&lt;/li>
&lt;li>&lt;strong>The public surface stays documentation-only.&lt;/strong> The lab&amp;rsquo;s public site and GitHub Pages carry docs; the tool plane (&lt;code>:30100&lt;/code>) and the kagent UI (&lt;code>:30500&lt;/code>) are never published there.&lt;/li>
&lt;/ul>
&lt;p>Contrast that with the sprawl diagram: six protocols implemented in a dozen editors, each holding long-lived secrets. Centralizing the &lt;em>tool plane&lt;/em> behind one governed proxy shrinks the credential footprint from &amp;ldquo;everywhere&amp;rdquo; to &amp;ldquo;one namespace, backed by Vault.&amp;rdquo;&lt;/p>
&lt;h3 id="what-it-does--and-what-it-doesnt-yet">What it does — and what it doesn&amp;rsquo;t, yet&lt;/h3>
&lt;p>It&amp;rsquo;s worth being precise about which of these properties are &lt;em>wired today&lt;/em> versus which are the gateway&amp;rsquo;s potential. On this lab, right now, the gateway is doing exactly six jobs:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Job&lt;/th>
&lt;th>How it shows up here&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Single front door&lt;/td>
&lt;td>One &lt;code>agentgateway-proxy&lt;/code> for models, MCP, and desktop — not a second proxy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MCP multiplex&lt;/td>
&lt;td>Seven Streamable HTTP targets behind one &lt;code>/mcp&lt;/code> URL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Name isolation&lt;/td>
&lt;td>&lt;code>prefixMode: Conditional&lt;/code> → &lt;code>fortigate_fg_…&lt;/code>, &lt;code>arista-ceos_…&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Partial failure&lt;/td>
&lt;td>&lt;code>failureMode: FailOpen&lt;/code> — a dead budget MCP doesn&amp;rsquo;t hide FortiGate tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hide ClusterIP&lt;/td>
&lt;td>Clients never need &lt;code>fortigate-mcp.kagent:8084&lt;/code>, only &lt;code>:30100&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Keep secrets in Vault&lt;/td>
&lt;td>The gateway injects no device or cloud keys; the MCP pods already hold ExternalSecrets&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>And, just as importantly, what is &lt;strong>not&lt;/strong> wired yet — so nobody mistakes this lab for a hardened deployment: there is &lt;strong>no client auth on &lt;code>/mcp&lt;/code>&lt;/strong> (it&amp;rsquo;s LAN, no bearer, same posture as &lt;code>/spark&lt;/code>), &lt;strong>no tool-level allow lists&lt;/strong>, and &lt;strong>no public-internet exposure&lt;/strong>. Anything on the LAN that can reach &lt;code>:30100&lt;/code> can call these tools. That&amp;rsquo;s an acceptable trade for a LAN lab; it would not be for production, and the honest move is to name it rather than imply a security boundary that isn&amp;rsquo;t there.&lt;/p>
&lt;h2 id="using-it-grok-bot-as-the-governed-client">Using it: Grok Bot as the governed client&lt;/h2>
&lt;p>The intended way to actually &lt;em>use&lt;/em> the multiplex is not to wire it into your editor — it&amp;rsquo;s to ask &lt;strong>Grok Bot&lt;/strong>, the k8s-viper agent, in plain language. It already has a jump onto Viper and calls &lt;code>http://127.0.0.1:30100/mcp&lt;/code> from the host, so you never paste a token and never publish &lt;code>/mcp&lt;/code>:&lt;/p>
&lt;blockquote>
&lt;ul>
&lt;li>&amp;ldquo;What is the BGP summary on spine1?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;List FortiGate policies that mention YouTube.&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;AWS MTD spend in us-east-2.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>One sentence in; the agent picks the right namespaced tool (&lt;code>arista-ceos_bgp_summary&lt;/code>, &lt;code>fortigate_*&lt;/code>, &lt;code>aws-budget_cost_month&lt;/code>), calls it through the gateway, and answers. You&amp;rsquo;re driving seven systems&amp;rsquo; worth of read-only tooling through a single chat, and the credentials for all of them stayed in Vault the entire time.&lt;/p>
&lt;p>If you &lt;em>do&lt;/em> want raw tools in a client, the same URL works from anything on the LAN — a Cursor &lt;code>mcpServers&lt;/code> entry pointed at &lt;code>http://172.16.10.135:30100/mcp&lt;/code>, or MCP Inspector over Streamable HTTP — with the standing rule that it never goes on a public machine. (A quick sanity check: &lt;code>GET /&lt;/code> on &lt;code>:30100&lt;/code> returns &lt;code>404 route not found&lt;/code>. That&amp;rsquo;s expected — the tools live at &lt;code>/mcp&lt;/code>, not the root.)&lt;/p>
&lt;h2 id="the-rules-that-keep-it-honest">The rules that keep it honest&lt;/h2>
&lt;p>The doc this is drawn from is blunt about its own guardrails, and they&amp;rsquo;re worth repeating because they&amp;rsquo;re what make &amp;ldquo;one door&amp;rdquo; defensible:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LAN only.&lt;/strong> Do not publish &lt;code>:30100&lt;/code> or the kagent UI &lt;code>:30500&lt;/code> on the public site.&lt;/li>
&lt;li>&lt;strong>Vault path names only in git.&lt;/strong> Never commit a secret value; never paste one into chat.&lt;/li>
&lt;li>&lt;strong>Don&amp;rsquo;t bump the pins to &amp;ldquo;fix&amp;rdquo; a missing tool.&lt;/strong> kagent &lt;code>0.10.0-rc2&lt;/code> and Substrate &lt;code>0.0.9&lt;/code> are pinned on purpose.&lt;/li>
&lt;li>&lt;strong>FailOpen is a feature, not a mask.&lt;/strong> A missing tool usually means a backend pod is down — check the Deployment, don&amp;rsquo;t assume the gateway is broken.&lt;/li>
&lt;/ul>
&lt;h2 id="the-takeaway">The takeaway&lt;/h2>
&lt;p>MCP is going to give every system in your environment an agent-friendly interface. That&amp;rsquo;s the good news and the scaling problem in one sentence: the moment you have several, &amp;ldquo;how does each client reach each server, and who holds the keys?&amp;rdquo; becomes the real design question.&lt;/p>
&lt;p>Multiplexing answers it by inverting the topology. Instead of &lt;em>N clients × M servers&lt;/em> each carrying credentials, you get &lt;strong>one front door&lt;/strong>, one client identity, and M servers that never leave the cluster with their secrets sealed in Vault. The client experience gets simpler — one URL, every tool — and the security posture gets &lt;em>tighter&lt;/em>, not looser. On Viper that&amp;rsquo;s 185 tools across seven systems, reachable by asking Grok Bot a question in English, with not a single device password ever leaving the cluster.&lt;/p>
&lt;p>One door. Every tool. Keys stay home.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The multiplex config, the full server catalog, and the live captures in this article are documented in &lt;a href="https://github.com/sebbycorp/k8s-viper/tree/main/docs/mcp-servers">k8s-viper / docs/mcp-servers&lt;/a>, with the individual MCP servers in &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos">sebbycorp/kagent-agent-substrate-demos&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>The After-Hours Kill Switch: Daytime Claude, Nighttime Grok, One CEL Expression</title><link>https://maniak.io/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/</guid><description>&lt;p>Here&amp;rsquo;s a decision every team using premium models eventually faces, usually after reading a bill: &lt;em>the expensive model is worth it during the workday, and it very much is not worth it at 2 a.m. for a batch job nobody is watching.&lt;/em> Claude&amp;rsquo;s quality earns its price when a human is in the loop. Overnight, a cheaper, faster model is fine — and the difference, multiplied across every off-hours request, is real money.&lt;/p>
&lt;p>The naive fix is an &lt;code>if&lt;/code> statement in every client: check the hour, pick a model. Now that logic lives in a dozen codebases, each with its own idea of &amp;ldquo;after hours,&amp;rdquo; each needing a redeploy to change. The better fix is to make the client dumb and the &lt;strong>gateway&lt;/strong> smart: let every app always ask for the same model by name, and let one policy decide where that request actually goes.&lt;/p>
&lt;p>This is a walk through a small, complete &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> demo — &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/12-after-hours-kill-switch">&lt;code>12-after-hours-kill-switch&lt;/code>&lt;/a> — that does exactly that. Clients always send &lt;code>&amp;quot;model&amp;quot;: &amp;quot;claude&amp;quot;&lt;/code>. During daytime Toronto hours the gateway routes to Anthropic&amp;rsquo;s Claude Sonnet. After 7pm it silently rewrites every one of those requests to xAI&amp;rsquo;s Grok, until 8am. Same URL, same model name, no SDK change, no cron, no client-side clock check. The switch is a single CEL expression in config.&lt;/p>
&lt;h2 id="the-idea-the-client-asks-the-gateway-decides">The idea: the client asks, the gateway decides&lt;/h2>
&lt;p>The whole demo turns on one inversion of control. The client doesn&amp;rsquo;t name a provider — it names an &lt;strong>intent&lt;/strong>: &lt;code>claude&lt;/code>. That&amp;rsquo;s a &lt;em>public virtual model&lt;/em>. What it resolves to is the gateway&amp;rsquo;s decision, re-evaluated on every single request.&lt;/p>
&lt;div class="mermaid">flowchart TB
 c[&amp;#34;Client&amp;lt;br/&amp;gt;POST /v1/chat/completions&amp;lt;br/&amp;gt;model: claude&amp;#34;]
 g[&amp;#34;agentgateway :4000&amp;lt;br/&amp;gt;virtual model &amp;#39;claude&amp;#39;&amp;#34;]
 cond{&amp;#34;daytime in Toronto?&amp;lt;br/&amp;gt;(and not forced off)&amp;#34;}
 cloud[&amp;#34;Anthropic&amp;lt;br/&amp;gt;claude-sonnet-4-6&amp;#34;]
 grok[&amp;#34;xAI&amp;lt;br/&amp;gt;grok-4.6&amp;#34;]
 c --&amp;gt; g --&amp;gt; cond
 cond --&amp;gt;|&amp;#34;yes — daytime quality&amp;#34;| cloud
 cond --&amp;gt;|&amp;#34;no — after-hours kill switch&amp;#34;| grok
 style g fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style cond fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style cloud fill:#F1EFE9,stroke:#17181C,color:#17181C
 style grok fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>The application code never changes between noon and midnight. It always sends &lt;code>claude&lt;/code>. The gateway is the only thing that knows there are two backends behind that name — and which one is in force right now.&lt;/p>
&lt;p>You can see the shape of it in the standalone admin UI&amp;rsquo;s overview: LLM enabled, &lt;strong>one virtual model&lt;/strong> in front of &lt;strong>two backend models&lt;/strong>, backed by &lt;strong>two shared providers&lt;/strong>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-home.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-home.png" alt="agentgateway admin UI Gateway Overview: LLM enabled with 2 models, 1 virtual model, 2 shared providers; MCP not enabled; Traffic enabled with 1 gateway" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>The standalone admin UI at &lt;code>:15000/ui/&lt;/code> — one virtual model, two backends, two providers. MCP isn&amp;rsquo;t part of this demo; it&amp;rsquo;s pure LLM routing.&lt;/em>&lt;/p>
&lt;h2 id="how-its-built-virtual-model--conditional-routing--cel">How it&amp;rsquo;s built: virtual model + conditional routing + CEL&lt;/h2>
&lt;p>Three pieces of &lt;code>config.yaml&lt;/code> compose the switch.&lt;/p>
&lt;p>&lt;strong>Providers&lt;/strong> hold the credentials, once — just the two this demo needs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$ANTHROPIC_API_KEY&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$XAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Concrete models&lt;/strong> are the real upstreams — and, crucially, both are &lt;code>visibility: internal&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">visibility&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">reference&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-sonnet-4-6 }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">visibility&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">reference&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4.6 }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>The virtual model&lt;/strong> &lt;code>claude&lt;/code> is the only public name, and it routes &lt;em>conditionally&lt;/em>. Its &lt;code>routing.conditional.targets&lt;/code> are a list of &lt;a href="https://agentgateway.dev/docs/standalone/latest/reference/cel/">CEL&lt;/a> &lt;code>when&lt;/code> expressions, evaluated top to bottom — &lt;strong>first match wins&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">virtualModels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">conditional&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> default(request.headers[&amp;#34;x-force-after-hours&amp;#34;], &amp;#34;&amp;#34;) != &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;amp;&amp;amp; timestamp(request.startTime).getHours() &amp;gt;= 12
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;amp;&amp;amp; timestamp(request.startTime).getHours() &amp;lt; 23&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the fallback — always matches&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read it as a policy sentence: &lt;em>use Anthropic only if nobody forced the switch and it&amp;rsquo;s inside the daytime UTC window; otherwise fall through to Grok.&lt;/em> That final &lt;code>when: &amp;quot;true&amp;quot;&lt;/code> is the safety net — it always matches, so no request ever fails to route somewhere.&lt;/p>
&lt;p>The admin UI renders the same structure: two backend models with no policy, and the &lt;code>claude&lt;/code> virtual model marked &lt;strong>conditional&lt;/strong> with &lt;strong>2 rules&lt;/strong>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-models.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-models.png" alt="agentgateway LLM Models list: anthropic-claude → claude-sonnet-4-6 (policy none), xai-grok → grok-4.6 (policy none), and claude marked Virtual with &amp;amp;lsquo;2 rules&amp;amp;rsquo; and policy state &amp;amp;lsquo;conditional&amp;amp;rsquo;" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>&lt;code>claude&lt;/code> is a Virtual model with a Conditional policy (2 rules). The two concrete backends carry no policy of their own — the routing lives entirely on the virtual name.&lt;/em>&lt;/p>
&lt;div class="mermaid">flowchart LR
 pub[&amp;#34;Public name&amp;lt;br/&amp;gt;claude&amp;#34;]
 t1{&amp;#34;target 1&amp;lt;br/&amp;gt;when: daytime &amp;amp;amp; not forced&amp;#34;}
 t2[&amp;#34;target 2&amp;lt;br/&amp;gt;when: true&amp;#34;]
 m1[&amp;#34;anthropic-claude&amp;lt;br/&amp;gt;(internal)&amp;#34;]
 m2[&amp;#34;xai-grok&amp;lt;br/&amp;gt;(internal)&amp;#34;]
 pub --&amp;gt; t1
 t1 --&amp;gt;|match| m1
 t1 --&amp;gt;|no match| t2 --&amp;gt; m2
 style pub fill:#FFF7D6,stroke:#17181C,color:#17181C
 style t1 fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style m1 fill:#F1EFE9,stroke:#17181C,color:#17181C
 style m2 fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;h2 id="why-the-switch-cant-be-dodged">Why the switch can&amp;rsquo;t be dodged&lt;/h2>
&lt;p>Here&amp;rsquo;s the detail that turns a routing trick into a &lt;em>control&lt;/em>: both concrete models are &lt;code>visibility: internal&lt;/code>. A client cannot send &lt;code>&amp;quot;model&amp;quot;: &amp;quot;anthropic-claude&amp;quot;&lt;/code> to force Claude at 3 a.m., and cannot name &lt;code>xai-grok&lt;/code> either — internal models aren&amp;rsquo;t addressable from outside. The only door into the gateway is the public name &lt;code>claude&lt;/code>, and it always runs the CEL gauntlet first.&lt;/p>
&lt;p>That&amp;rsquo;s the difference between a convenience and a guardrail. If clients could name the real upstream, &amp;ldquo;after-hours kill switch&amp;rdquo; would be a polite suggestion. Because they can&amp;rsquo;t, it&amp;rsquo;s enforced.&lt;/p>
&lt;h2 id="the-timezone-footgun-read-this-before-you-copy-the-cel">The timezone footgun (read this before you copy the CEL)&lt;/h2>
&lt;p>The one thing that trips everyone up: &lt;code>timestamp(request.startTime).getHours()&lt;/code> returns the hour in &lt;strong>UTC&lt;/strong>, not your local time. The demo&amp;rsquo;s business hours are America/Toronto, which in August 2026 is EDT (UTC−4), so the config translates the intended local window into UTC:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Local (America/Toronto)&lt;/th>
&lt;th>UTC &lt;code>getHours()&lt;/code>&lt;/th>
&lt;th>Served&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Daytime &lt;code>08:00&lt;/code>–&lt;code>18:59&lt;/code>&lt;/td>
&lt;td>&lt;code>12&lt;/code>–&lt;code>22&lt;/code>&lt;/td>
&lt;td>Anthropic &lt;code>claude-sonnet-4-6&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>After-hours &lt;code>19:00&lt;/code>–&lt;code>07:59&lt;/code>&lt;/td>
&lt;td>&lt;code>23&lt;/code>–&lt;code>11&lt;/code>&lt;/td>
&lt;td>xAI &lt;code>grok-4.6&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s why the CEL reads &lt;code>&amp;gt;= 12 &amp;amp;&amp;amp; &amp;lt; 23&lt;/code> rather than &lt;code>&amp;gt;= 8 &amp;amp;&amp;amp; &amp;lt; 19&lt;/code>. If you lift this pattern, convert your own local window to UTC — and remember it drifts by an hour across DST, so the boundaries you hardcode in August aren&amp;rsquo;t the ones you want in January.&lt;/p>
&lt;h2 id="proof-a-711pm-request-served-grok">Proof: a 7:11pm request served Grok&lt;/h2>
&lt;p>This is the moment the switch fires. A client asked for &lt;code>claude&lt;/code> with &lt;strong>no extra headers&lt;/strong> at &lt;strong>7:11pm Toronto&lt;/strong> — past the 7pm cutoff — and the gateway served &lt;strong>&lt;code>grok-4.6&lt;/code>&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-live-test.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-live-test.png" alt="After-hours kill switch live test card: asked for model claude with no extra headers; Toronto time 2026-08-18 19:11 UTC-04:00; gateway served grok-4.6; reply &amp;amp;lsquo;ok&amp;amp;rsquo;; POST to 127.0.0.1:4000" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live, 2026-08-18 at 19:11 Toronto. Client sent &lt;code>claude&lt;/code>; the JSON &lt;code>model&lt;/code> field came back &lt;code>grok-4.6&lt;/code>. The rewrite is invisible to the caller — only the response body reveals it.&lt;/em>&lt;/p>
&lt;p>There&amp;rsquo;s a subtlety worth calling out, visible in the admin UI&amp;rsquo;s Chat Playground: the playground labels the request with the &lt;strong>public&lt;/strong> name &lt;code>claude&lt;/code> even on a call that Grok actually served. The rewrite isn&amp;rsquo;t in the label — it&amp;rsquo;s in the response&amp;rsquo;s &lt;code>model&lt;/code> field.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-playground.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-playground.png" alt="agentgateway Chat Playground: model selector set to &amp;amp;lsquo;claude&amp;amp;rsquo;, a &amp;amp;lsquo;Reply with exactly: ok&amp;amp;rsquo; prompt, and an &amp;amp;lsquo;ok&amp;amp;rsquo; response chip labeled claude, 1.4s, 220 in / 1 out" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>The Playground always shows the public name &lt;code>claude&lt;/code>. To see which backend served a call, read the &lt;code>model&lt;/code> field in the response JSON (&lt;code>claude-sonnet-4-6&lt;/code> = Anthropic, &lt;code>grok-4.6&lt;/code> = xAI) — or &lt;code>gen_ai.provider.name&lt;/code> in the gateway log.&lt;/em>&lt;/p>
&lt;p>To demonstrate the night path without waiting until 7pm, the config honors one break-glass header — &lt;code>x-force-after-hours: true&lt;/code> — which forces Grok at any hour:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-force-after-hours: true&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;claude&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Reply with exactly: ok&amp;#34;}],&amp;#34;max_tokens&amp;#34;:16}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model, content: .choices[0].message.content}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At noon that returns &lt;code>grok-4.6&lt;/code> instead of &lt;code>claude-sonnet-4-6&lt;/code> — the same rewrite the clock would trigger at night, on demand.&lt;/p>
&lt;h2 id="why-this-is-worth-doing">Why this is worth doing&lt;/h2>
&lt;p>Step back from the specifics and the pattern is a small piece of platform governance with an outsized payoff:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>One switch, whole fleet.&lt;/strong> Every client that speaks OpenAI-compatible HTTP is governed by this one config. Changing the after-hours destination — or the window — is a config edit, not a fleet-wide redeploy.&lt;/li>
&lt;li>&lt;strong>Cost control on a clock.&lt;/strong> Premium models cost real money per token. Confining Claude to business hours and defaulting off-hours traffic to a cheaper model is a lever you can pull without asking a dozen teams to touch code.&lt;/li>
&lt;li>&lt;strong>Provider-agnostic clients.&lt;/strong> Application code names an intent (&lt;code>claude&lt;/code>), not a vendor. Swapping the upstream, adding a failover, or retiring a model id happens in the gateway — the demo even pins &lt;code>grok-4.6&lt;/code> because xAI retired the older &lt;code>grok-2-latest&lt;/code> the docs still show. Clients never noticed.&lt;/li>
&lt;li>&lt;strong>Enforced, not advisory.&lt;/strong> Internal-visibility upstreams mean the policy is a wall, not a naming convention. There&amp;rsquo;s no client-side flag to disrespect.&lt;/li>
&lt;/ul>
&lt;p>The honest scope: this is a standalone lab demo of &lt;em>routing&lt;/em> governance. The &lt;code>/v1&lt;/code> listener here has no client auth (it&amp;rsquo;s a local demo), and the &amp;ldquo;kill switch&amp;rdquo; governs &lt;em>which model&lt;/em> a request reaches, not &lt;em>whether the caller is allowed&lt;/em> — that&amp;rsquo;s a separate policy layer. What it demonstrates cleanly is the principle: the decision of where model traffic goes belongs in the gateway, expressed as policy, re-evaluated per request.&lt;/p>
&lt;h2 id="the-takeaway">The takeaway&lt;/h2>
&lt;p>A kill switch is only useful if it lives in one place and someone can actually reach it. Scattering model-selection logic across every client gives you neither. Pulling it into agentgateway as one virtual model and one CEL expression gives you both: a single, enforced, per-request decision about where your LLM traffic goes — flippable by the clock or one break-glass header, with not a single client redeploy.&lt;/p>
&lt;p>Business hours, Claude. After hours, Grok. Same &lt;code>claude&lt;/code> every time — and the clients never have to know which one answered.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Full config, the curl cases, the run scripts, and the live screenshots are in &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/12-after-hours-kill-switch">sebbycorp/agentgateway-demos / 12-after-hours-kill-switch&lt;/a>. Background on virtual models and conditional routing is in the &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/virtual-models/">agentgateway standalone docs&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>Here&amp;rsquo;s a decision every team using premium models eventually faces, usually after reading a bill: &lt;em>the expensive model is worth it during the workday, and it very much is not worth it at 2 a.m. for a batch job nobody is watching.&lt;/em> Claude&amp;rsquo;s quality earns its price when a human is in the loop. Overnight, a cheaper, faster model is fine — and the difference, multiplied across every off-hours request, is real money.&lt;/p>
&lt;p>The naive fix is an &lt;code>if&lt;/code> statement in every client: check the hour, pick a model. Now that logic lives in a dozen codebases, each with its own idea of &amp;ldquo;after hours,&amp;rdquo; each needing a redeploy to change. The better fix is to make the client dumb and the &lt;strong>gateway&lt;/strong> smart: let every app always ask for the same model by name, and let one policy decide where that request actually goes.&lt;/p>
&lt;p>This is a walk through a small, complete &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> demo — &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/12-after-hours-kill-switch">&lt;code>12-after-hours-kill-switch&lt;/code>&lt;/a> — that does exactly that. Clients always send &lt;code>&amp;quot;model&amp;quot;: &amp;quot;claude&amp;quot;&lt;/code>. During daytime Toronto hours the gateway routes to Anthropic&amp;rsquo;s Claude Sonnet. After 7pm it silently rewrites every one of those requests to xAI&amp;rsquo;s Grok, until 8am. Same URL, same model name, no SDK change, no cron, no client-side clock check. The switch is a single CEL expression in config.&lt;/p>
&lt;h2 id="the-idea-the-client-asks-the-gateway-decides">The idea: the client asks, the gateway decides&lt;/h2>
&lt;p>The whole demo turns on one inversion of control. The client doesn&amp;rsquo;t name a provider — it names an &lt;strong>intent&lt;/strong>: &lt;code>claude&lt;/code>. That&amp;rsquo;s a &lt;em>public virtual model&lt;/em>. What it resolves to is the gateway&amp;rsquo;s decision, re-evaluated on every single request.&lt;/p>
&lt;div class="mermaid">flowchart TB
 c[&amp;#34;Client&amp;lt;br/&amp;gt;POST /v1/chat/completions&amp;lt;br/&amp;gt;model: claude&amp;#34;]
 g[&amp;#34;agentgateway :4000&amp;lt;br/&amp;gt;virtual model &amp;#39;claude&amp;#39;&amp;#34;]
 cond{&amp;#34;daytime in Toronto?&amp;lt;br/&amp;gt;(and not forced off)&amp;#34;}
 cloud[&amp;#34;Anthropic&amp;lt;br/&amp;gt;claude-sonnet-4-6&amp;#34;]
 grok[&amp;#34;xAI&amp;lt;br/&amp;gt;grok-4.6&amp;#34;]
 c --&amp;gt; g --&amp;gt; cond
 cond --&amp;gt;|&amp;#34;yes — daytime quality&amp;#34;| cloud
 cond --&amp;gt;|&amp;#34;no — after-hours kill switch&amp;#34;| grok
 style g fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style cond fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style cloud fill:#F1EFE9,stroke:#17181C,color:#17181C
 style grok fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>The application code never changes between noon and midnight. It always sends &lt;code>claude&lt;/code>. The gateway is the only thing that knows there are two backends behind that name — and which one is in force right now.&lt;/p>
&lt;p>You can see the shape of it in the standalone admin UI&amp;rsquo;s overview: LLM enabled, &lt;strong>one virtual model&lt;/strong> in front of &lt;strong>two backend models&lt;/strong>, backed by &lt;strong>two shared providers&lt;/strong>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-home.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-home.png" alt="agentgateway admin UI Gateway Overview: LLM enabled with 2 models, 1 virtual model, 2 shared providers; MCP not enabled; Traffic enabled with 1 gateway" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>The standalone admin UI at &lt;code>:15000/ui/&lt;/code> — one virtual model, two backends, two providers. MCP isn&amp;rsquo;t part of this demo; it&amp;rsquo;s pure LLM routing.&lt;/em>&lt;/p>
&lt;h2 id="how-its-built-virtual-model--conditional-routing--cel">How it&amp;rsquo;s built: virtual model + conditional routing + CEL&lt;/h2>
&lt;p>Three pieces of &lt;code>config.yaml&lt;/code> compose the switch.&lt;/p>
&lt;p>&lt;strong>Providers&lt;/strong> hold the credentials, once — just the two this demo needs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$ANTHROPIC_API_KEY&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$XAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Concrete models&lt;/strong> are the real upstreams — and, crucially, both are &lt;code>visibility: internal&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">visibility&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">reference&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-sonnet-4-6 }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">visibility&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">internal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">reference&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4.6 }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>The virtual model&lt;/strong> &lt;code>claude&lt;/code> is the only public name, and it routes &lt;em>conditionally&lt;/em>. Its &lt;code>routing.conditional.targets&lt;/code> are a list of &lt;a href="https://agentgateway.dev/docs/standalone/latest/reference/cel/">CEL&lt;/a> &lt;code>when&lt;/code> expressions, evaluated top to bottom — &lt;strong>first match wins&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">virtualModels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">conditional&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> default(request.headers[&amp;#34;x-force-after-hours&amp;#34;], &amp;#34;&amp;#34;) != &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;amp;&amp;amp; timestamp(request.startTime).getHours() &amp;gt;= 12
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;amp;&amp;amp; timestamp(request.startTime).getHours() &amp;lt; 23&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">when&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the fallback — always matches&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read it as a policy sentence: &lt;em>use Anthropic only if nobody forced the switch and it&amp;rsquo;s inside the daytime UTC window; otherwise fall through to Grok.&lt;/em> That final &lt;code>when: &amp;quot;true&amp;quot;&lt;/code> is the safety net — it always matches, so no request ever fails to route somewhere.&lt;/p>
&lt;p>The admin UI renders the same structure: two backend models with no policy, and the &lt;code>claude&lt;/code> virtual model marked &lt;strong>conditional&lt;/strong> with &lt;strong>2 rules&lt;/strong>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-models.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-models.png" alt="agentgateway LLM Models list: anthropic-claude → claude-sonnet-4-6 (policy none), xai-grok → grok-4.6 (policy none), and claude marked Virtual with &amp;amp;lsquo;2 rules&amp;amp;rsquo; and policy state &amp;amp;lsquo;conditional&amp;amp;rsquo;" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>&lt;code>claude&lt;/code> is a Virtual model with a Conditional policy (2 rules). The two concrete backends carry no policy of their own — the routing lives entirely on the virtual name.&lt;/em>&lt;/p>
&lt;div class="mermaid">flowchart LR
 pub[&amp;#34;Public name&amp;lt;br/&amp;gt;claude&amp;#34;]
 t1{&amp;#34;target 1&amp;lt;br/&amp;gt;when: daytime &amp;amp;amp; not forced&amp;#34;}
 t2[&amp;#34;target 2&amp;lt;br/&amp;gt;when: true&amp;#34;]
 m1[&amp;#34;anthropic-claude&amp;lt;br/&amp;gt;(internal)&amp;#34;]
 m2[&amp;#34;xai-grok&amp;lt;br/&amp;gt;(internal)&amp;#34;]
 pub --&amp;gt; t1
 t1 --&amp;gt;|match| m1
 t1 --&amp;gt;|no match| t2 --&amp;gt; m2
 style pub fill:#FFF7D6,stroke:#17181C,color:#17181C
 style t1 fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style m1 fill:#F1EFE9,stroke:#17181C,color:#17181C
 style m2 fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;h2 id="why-the-switch-cant-be-dodged">Why the switch can&amp;rsquo;t be dodged&lt;/h2>
&lt;p>Here&amp;rsquo;s the detail that turns a routing trick into a &lt;em>control&lt;/em>: both concrete models are &lt;code>visibility: internal&lt;/code>. A client cannot send &lt;code>&amp;quot;model&amp;quot;: &amp;quot;anthropic-claude&amp;quot;&lt;/code> to force Claude at 3 a.m., and cannot name &lt;code>xai-grok&lt;/code> either — internal models aren&amp;rsquo;t addressable from outside. The only door into the gateway is the public name &lt;code>claude&lt;/code>, and it always runs the CEL gauntlet first.&lt;/p>
&lt;p>That&amp;rsquo;s the difference between a convenience and a guardrail. If clients could name the real upstream, &amp;ldquo;after-hours kill switch&amp;rdquo; would be a polite suggestion. Because they can&amp;rsquo;t, it&amp;rsquo;s enforced.&lt;/p>
&lt;h2 id="the-timezone-footgun-read-this-before-you-copy-the-cel">The timezone footgun (read this before you copy the CEL)&lt;/h2>
&lt;p>The one thing that trips everyone up: &lt;code>timestamp(request.startTime).getHours()&lt;/code> returns the hour in &lt;strong>UTC&lt;/strong>, not your local time. The demo&amp;rsquo;s business hours are America/Toronto, which in August 2026 is EDT (UTC−4), so the config translates the intended local window into UTC:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Local (America/Toronto)&lt;/th>
&lt;th>UTC &lt;code>getHours()&lt;/code>&lt;/th>
&lt;th>Served&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Daytime &lt;code>08:00&lt;/code>–&lt;code>18:59&lt;/code>&lt;/td>
&lt;td>&lt;code>12&lt;/code>–&lt;code>22&lt;/code>&lt;/td>
&lt;td>Anthropic &lt;code>claude-sonnet-4-6&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>After-hours &lt;code>19:00&lt;/code>–&lt;code>07:59&lt;/code>&lt;/td>
&lt;td>&lt;code>23&lt;/code>–&lt;code>11&lt;/code>&lt;/td>
&lt;td>xAI &lt;code>grok-4.6&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s why the CEL reads &lt;code>&amp;gt;= 12 &amp;amp;&amp;amp; &amp;lt; 23&lt;/code> rather than &lt;code>&amp;gt;= 8 &amp;amp;&amp;amp; &amp;lt; 19&lt;/code>. If you lift this pattern, convert your own local window to UTC — and remember it drifts by an hour across DST, so the boundaries you hardcode in August aren&amp;rsquo;t the ones you want in January.&lt;/p>
&lt;h2 id="proof-a-711pm-request-served-grok">Proof: a 7:11pm request served Grok&lt;/h2>
&lt;p>This is the moment the switch fires. A client asked for &lt;code>claude&lt;/code> with &lt;strong>no extra headers&lt;/strong> at &lt;strong>7:11pm Toronto&lt;/strong> — past the 7pm cutoff — and the gateway served &lt;strong>&lt;code>grok-4.6&lt;/code>&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-live-test.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-live-test.png" alt="After-hours kill switch live test card: asked for model claude with no extra headers; Toronto time 2026-08-18 19:11 UTC-04:00; gateway served grok-4.6; reply &amp;amp;lsquo;ok&amp;amp;rsquo;; POST to 127.0.0.1:4000" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live, 2026-08-18 at 19:11 Toronto. Client sent &lt;code>claude&lt;/code>; the JSON &lt;code>model&lt;/code> field came back &lt;code>grok-4.6&lt;/code>. The rewrite is invisible to the caller — only the response body reveals it.&lt;/em>&lt;/p>
&lt;p>There&amp;rsquo;s a subtlety worth calling out, visible in the admin UI&amp;rsquo;s Chat Playground: the playground labels the request with the &lt;strong>public&lt;/strong> name &lt;code>claude&lt;/code> even on a call that Grok actually served. The rewrite isn&amp;rsquo;t in the label — it&amp;rsquo;s in the response&amp;rsquo;s &lt;code>model&lt;/code> field.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-playground.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-after-hours-llm-kill-switch-agentgateway/agw-ui-playground.png" alt="agentgateway Chat Playground: model selector set to &amp;amp;lsquo;claude&amp;amp;rsquo;, a &amp;amp;lsquo;Reply with exactly: ok&amp;amp;rsquo; prompt, and an &amp;amp;lsquo;ok&amp;amp;rsquo; response chip labeled claude, 1.4s, 220 in / 1 out" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>The Playground always shows the public name &lt;code>claude&lt;/code>. To see which backend served a call, read the &lt;code>model&lt;/code> field in the response JSON (&lt;code>claude-sonnet-4-6&lt;/code> = Anthropic, &lt;code>grok-4.6&lt;/code> = xAI) — or &lt;code>gen_ai.provider.name&lt;/code> in the gateway log.&lt;/em>&lt;/p>
&lt;p>To demonstrate the night path without waiting until 7pm, the config honors one break-glass header — &lt;code>x-force-after-hours: true&lt;/code> — which forces Grok at any hour:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-force-after-hours: true&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;claude&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Reply with exactly: ok&amp;#34;}],&amp;#34;max_tokens&amp;#34;:16}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model, content: .choices[0].message.content}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>At noon that returns &lt;code>grok-4.6&lt;/code> instead of &lt;code>claude-sonnet-4-6&lt;/code> — the same rewrite the clock would trigger at night, on demand.&lt;/p>
&lt;h2 id="why-this-is-worth-doing">Why this is worth doing&lt;/h2>
&lt;p>Step back from the specifics and the pattern is a small piece of platform governance with an outsized payoff:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>One switch, whole fleet.&lt;/strong> Every client that speaks OpenAI-compatible HTTP is governed by this one config. Changing the after-hours destination — or the window — is a config edit, not a fleet-wide redeploy.&lt;/li>
&lt;li>&lt;strong>Cost control on a clock.&lt;/strong> Premium models cost real money per token. Confining Claude to business hours and defaulting off-hours traffic to a cheaper model is a lever you can pull without asking a dozen teams to touch code.&lt;/li>
&lt;li>&lt;strong>Provider-agnostic clients.&lt;/strong> Application code names an intent (&lt;code>claude&lt;/code>), not a vendor. Swapping the upstream, adding a failover, or retiring a model id happens in the gateway — the demo even pins &lt;code>grok-4.6&lt;/code> because xAI retired the older &lt;code>grok-2-latest&lt;/code> the docs still show. Clients never noticed.&lt;/li>
&lt;li>&lt;strong>Enforced, not advisory.&lt;/strong> Internal-visibility upstreams mean the policy is a wall, not a naming convention. There&amp;rsquo;s no client-side flag to disrespect.&lt;/li>
&lt;/ul>
&lt;p>The honest scope: this is a standalone lab demo of &lt;em>routing&lt;/em> governance. The &lt;code>/v1&lt;/code> listener here has no client auth (it&amp;rsquo;s a local demo), and the &amp;ldquo;kill switch&amp;rdquo; governs &lt;em>which model&lt;/em> a request reaches, not &lt;em>whether the caller is allowed&lt;/em> — that&amp;rsquo;s a separate policy layer. What it demonstrates cleanly is the principle: the decision of where model traffic goes belongs in the gateway, expressed as policy, re-evaluated per request.&lt;/p>
&lt;h2 id="the-takeaway">The takeaway&lt;/h2>
&lt;p>A kill switch is only useful if it lives in one place and someone can actually reach it. Scattering model-selection logic across every client gives you neither. Pulling it into agentgateway as one virtual model and one CEL expression gives you both: a single, enforced, per-request decision about where your LLM traffic goes — flippable by the clock or one break-glass header, with not a single client redeploy.&lt;/p>
&lt;p>Business hours, Claude. After hours, Grok. Same &lt;code>claude&lt;/code> every time — and the clients never have to know which one answered.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Full config, the curl cases, the run scripts, and the live screenshots are in &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/12-after-hours-kill-switch">sebbycorp/agentgateway-demos / 12-after-hours-kill-switch&lt;/a>. Background on virtual models and conditional routing is in the &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/virtual-models/">agentgateway standalone docs&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>The Art of the Possible: Governed Sandbox Agents, and an Arista Operator That Can't Go Rogue</title><link>https://maniak.io/articles/2026-08-18-governed-sandbox-agents-kagent-arista/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-18-governed-sandbox-agents-kagent-arista/</guid><description>&lt;p>Ask a network engineer if they&amp;rsquo;d let an AI run commands on their production fabric and you&amp;rsquo;ll get a very short answer. The instinct is correct: a model that can &lt;code>configure&lt;/code>, &lt;code>clear ip bgp&lt;/code>, or &lt;code>shutdown&lt;/code> an interface is a model that can take down a data center between two tokens. For years that instinct has kept agents &lt;em>out&lt;/em> of the systems where they&amp;rsquo;d be most useful.&lt;/p>
&lt;p>But the instinct is aimed at the wrong thing. The danger was never &amp;ldquo;an AI touches the network.&amp;rdquo; The danger is &lt;strong>an AI with an ungoverned reach into the network&lt;/strong>. Remove the reach — cage it, scope it, watch it — and the same agent becomes something you&amp;rsquo;d actually want on call: a tireless read-only operator that can answer &amp;ldquo;is the fabric healthy?&amp;rdquo; at 3 a.m. without ever being able to break it.&lt;/p>
&lt;p>This article is about that cage, and the art of what becomes possible once you have one. The vehicle is a real &lt;a href="https://kagent.dev">kagent&lt;/a> demo — a &lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong> that operates a live 3-node &lt;strong>Arista cEOS&lt;/strong> fabric — and the point it proves generalizes far past networking: with a governed sandbox substrate, you can safely put an agent in front of almost anything.&lt;/p>
&lt;p>&lt;em>(If you want the foundations of the substrate itself — gVisor per-session isolation, snapshot/restore — start with &lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why Secure Sandbox Substrates Are the Future of AI Agents&lt;/a>. This piece builds on it and focuses on &lt;strong>governance&lt;/strong>: skills, tool catalogs, and blast-radius control.)&lt;/em>&lt;/p>
&lt;h2 id="this-time-the-agent-is-the-box">This time, the agent &lt;em>is&lt;/em> the box&lt;/h2>
&lt;p>Most agent demos talk to a system that already exists — a cloud API, a SaaS backend. This one is different: the thing being operated is a &lt;strong>live network fabric&lt;/strong> stood up just for the agent. Three Arista cEOS switches — &lt;code>spine1&lt;/code>, &lt;code>leaf1&lt;/code>, &lt;code>leaf2&lt;/code> — wired into a small Clos-ish underlay with Containerlab, running real EOS and real eBGP.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/clab-inspect.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/clab-inspect.png" alt="containerlab inspect showing three running nodes — clab-arista-ceos-leaf1, leaf2, and spine1 — all image ceos:4.33.9M on the 172.20.20.0/24 management network" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live on Viper, 2026-08-17. Three &lt;code>ceos:4.33.9M&lt;/code> nodes, &lt;code>running&lt;/code>, on the &lt;code>arista-ceos&lt;/code> management network.&lt;/em>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-show-version.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-show-version.png" alt="spine1 show version — Arista cEOSLab, software image version 4.33.9M-49063934.4339M, architecture x86_64, kernel 7.0.0-28-generic" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Real EOS, not a mock: spine1 running &lt;code>4.33.9M&lt;/code> on x86_64.&lt;/em>&lt;/p>
&lt;p>The topology is deliberately boring — boring is checkable. eBGP on point-to-point &lt;code>/31&lt;/code>s, one AS per switch, a loopback each. No EVPN, no MPLS, nothing an agent could misread.&lt;/p>
&lt;div class="mermaid">flowchart TB
 subgraph clab[&amp;#34;Containerlab fabric: arista-ceos&amp;#34;]
 spine1[&amp;#34;spine1&amp;lt;br/&amp;gt;AS 65000&amp;lt;br/&amp;gt;Lo0 10.255.0.1/32&amp;#34;]
 leaf1[&amp;#34;leaf1&amp;lt;br/&amp;gt;AS 65101&amp;lt;br/&amp;gt;Lo0 10.255.0.11/32&amp;#34;]
 leaf2[&amp;#34;leaf2&amp;lt;br/&amp;gt;AS 65102&amp;lt;br/&amp;gt;Lo0 10.255.0.12/32&amp;#34;]
 spine1 --&amp;gt;|&amp;#34;Ethernet1&amp;lt;br/&amp;gt;10.0.1.0/31&amp;#34;| leaf1
 spine1 --&amp;gt;|&amp;#34;Ethernet2&amp;lt;br/&amp;gt;10.0.2.0/31&amp;#34;| leaf2
 end
 style spine1 fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style leaf1 fill:#F1EFE9,stroke:#17181C,color:#17181C
 style leaf2 fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Now the interesting part: a kagent &lt;code>SandboxAgent&lt;/code> named &lt;code>arista-ceos&lt;/code> sits in front of this fabric as a &lt;strong>read-only network operator&lt;/strong>. It runs each conversation inside a gVisor actor, reaches the switches through a FastMCP server that speaks the EOS &lt;strong>eAPI&lt;/strong>, and answers questions in plain English. And — this is the whole game — it is governed on three independent layers so that &amp;ldquo;operator&amp;rdquo; can never quietly become &amp;ldquo;attacker.&amp;rdquo;&lt;/p>
&lt;h2 id="governance-layer-1--skills-that-encode-how-to-behave">Governance layer 1 — skills that encode &lt;em>how to behave&lt;/em>&lt;/h2>
&lt;p>An agent&amp;rsquo;s tools tell it &lt;em>what it can do&lt;/em>. Its &lt;strong>skills&lt;/strong> tell it &lt;em>how it should act&lt;/em>. In kagent, skills are markdown that gets folded into the agent&amp;rsquo;s &lt;code>systemMessage&lt;/code> (kagent &lt;code>0.10.0-rc2&lt;/code> deliberately rejects a &lt;code>spec.skills&lt;/code> field on sandbox agents, so the instructions ride in the prompt). That markdown is where operational judgment — and hard guardrails — live.&lt;/p>
&lt;p>The Arista agent ships three skills — &lt;code>fabric&lt;/code>, &lt;code>routing&lt;/code>, and &lt;code>executive-brief&lt;/code> — and every one of them opens with the same standing rules. They read like a runbook written by someone who&amp;rsquo;s been burned:&lt;/p>
&lt;blockquote>
&lt;ul>
&lt;li>&lt;strong>Read-only.&lt;/strong> No &lt;code>configure&lt;/code>, no &lt;code>write&lt;/code>, no neighbor shutdown, no image upgrade.&lt;/li>
&lt;li>&lt;strong>Never invent&lt;/strong> BGP state, prefixes, LLDP neighbors, or ping results. If eAPI fails or returns empty, say so.&lt;/li>
&lt;li>&lt;strong>Never print&lt;/strong> eAPI passwords, basic-auth headers, or Vault tokens.&lt;/li>
&lt;li>No generic CLI dump of the full running-config — it can include the lab AAA line. Prefer the named tools.&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>Notice what these rules are doing. &amp;ldquo;Never invent BGP state&amp;rdquo; turns the model&amp;rsquo;s most dangerous failure mode — a confident hallucination — into a contract: &lt;em>if the tool didn&amp;rsquo;t return it, don&amp;rsquo;t say it&lt;/em>. &amp;ldquo;Never print passwords&amp;rdquo; is a data-exfiltration guard written in English. &amp;ldquo;Prefer the named tools over a raw config dump&amp;rdquo; keeps secrets out of the transcript by construction. The skill even dictates the &lt;strong>shape&lt;/strong> of a good answer — lead with one sentence of fabric health, then a short table — so the agent is useful &lt;em>and&lt;/em> predictable.&lt;/p>
&lt;p>Skills are governance you can read, diff, and review in a pull request. That&amp;rsquo;s a very different thing from hoping a general-purpose model behaves.&lt;/p>
&lt;h2 id="governance-layer-2--a-curated-read-only-tool-catalog">Governance layer 2 — a curated, read-only tool catalog&lt;/h2>
&lt;p>Skills are the soft boundary. The &lt;strong>tool catalog&lt;/strong> is the hard one. The agent&amp;rsquo;s entire vocabulary for touching the fabric is six eAPI wrappers, and every one of them is a &lt;code>show&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>EOS command&lt;/th>
&lt;th>Scope&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>arista_inventory&lt;/code>&lt;/td>
&lt;td>&lt;code>show version&lt;/code>&lt;/td>
&lt;td>all nodes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_bgp_summary&lt;/code>&lt;/td>
&lt;td>&lt;code>show ip bgp summary&lt;/code>&lt;/td>
&lt;td>one or all nodes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_interfaces&lt;/code>&lt;/td>
&lt;td>&lt;code>show interfaces&lt;/code>&lt;/td>
&lt;td>single node&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_lldp_neighbors&lt;/code>&lt;/td>
&lt;td>&lt;code>show lldp neighbors&lt;/code>&lt;/td>
&lt;td>one or all nodes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_routes&lt;/code>&lt;/td>
&lt;td>&lt;code>show ip route [prefix]&lt;/code>&lt;/td>
&lt;td>single node, optional filter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_health&lt;/code>&lt;/td>
&lt;td>composed summary&lt;/td>
&lt;td>all nodes, concise&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is &lt;strong>no&lt;/strong> &lt;code>configure&lt;/code>, no &lt;code>clear&lt;/code>, no &lt;code>reload&lt;/code>, no generic &amp;ldquo;run any EOS command&amp;rdquo; escape hatch. This matters more than any prompt rule, because it&amp;rsquo;s not advisory — it&amp;rsquo;s the shape of the API. Even a perfectly jailbroken model, convinced by some injected instruction that it must reset a BGP session, has no verb to do it. The catalog simply doesn&amp;rsquo;t contain destruction.&lt;/p>
&lt;p>This is the same design decision behind every agent in the family: expose &lt;em>Describe / Get / View / Show&lt;/em>, and nothing that mutates. You lose nothing an operator needs for triage, and you remove the entire class of &amp;ldquo;the agent changed something&amp;rdquo; incidents.&lt;/p>
&lt;h2 id="governance-layer-3--the-sandbox-the-secrets-and-the-allowlist">Governance layer 3 — the sandbox, the secrets, and the allowlist&lt;/h2>
&lt;p>The third wall is the runtime itself.&lt;/p>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Operator chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;(the session)&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;arista-ceos-mcp&amp;#34;]
 mcp[&amp;#34;arista-ceos-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 fabric[&amp;#34;cEOS eAPI&amp;lt;br/&amp;gt;spine1 · leaf1 · leaf2&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/arista-ceos&amp;#34;]
 eso[&amp;#34;External Secrets&amp;lt;br/&amp;gt;Operator&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; fabric
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style vault fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Three things are happening on that diagram, each a control:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>gVisor isolation.&lt;/strong> The model session runs in a gVisor actor — a user-space kernel between the agent and the host. Untrusted tool output (and any injection riding in it) never gets to talk to the node&amp;rsquo;s real kernel.&lt;/li>
&lt;li>&lt;strong>Secrets the session never holds.&lt;/strong> The eAPI username and password live in &lt;strong>Vault&lt;/strong> at &lt;code>secret/platform/arista-ceos&lt;/code> and are synced by External Secrets Operator into the &lt;strong>MCP pod only&lt;/strong> — as &lt;code>username&lt;/code>, &lt;code>password&lt;/code>, and a &lt;code>hosts_json&lt;/code> map of allowlisted node → eAPI URL. The gVisor actor calls a tool; the tool holds the credential; the session never sees it.&lt;/li>
&lt;li>&lt;strong>An explicit node allowlist.&lt;/strong> The MCP server is pinned to &lt;code>ARISTA_ALLOWED_NODES=spine1,leaf1,leaf2&lt;/code>. Ask it about a switch that isn&amp;rsquo;t on the list and there&amp;rsquo;s nowhere for the request to go. (TLS verification is disabled &lt;em>for this lab only&lt;/em> — a documented lab shortcut, not a production posture.)&lt;/li>
&lt;/ul>
&lt;p>Stack the three layers and you get genuine defense in depth. A prompt injection has to get past the &lt;strong>skill rules&lt;/strong>, then find a destructive &lt;strong>tool that doesn&amp;rsquo;t exist&lt;/strong>, then escape a &lt;strong>gVisor sandbox&lt;/strong>, then reach a credential that &lt;strong>isn&amp;rsquo;t in the session&lt;/strong>, to touch a node that &lt;strong>isn&amp;rsquo;t on the allowlist&lt;/strong>. Each wall is boring on its own. Together they turn &amp;ldquo;an AI on the network&amp;rdquo; from reckless into routine.&lt;/p>
&lt;h2 id="watch-it-actually-work">Watch it actually work&lt;/h2>
&lt;p>Here&amp;rsquo;s the agent answering a real operator question — &lt;em>&amp;ldquo;What is the BGP summary on spine1?&amp;rdquo;&lt;/em> — with exactly &lt;strong>one&lt;/strong> tool call to &lt;code>arista_bgp_summary&lt;/code>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/kagent-arista-bgp-chat.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/kagent-arista-bgp-chat.png" alt="kagent chat: the arista-ceos agent answers &amp;amp;lsquo;What is the BGP summary on spine1?&amp;amp;rsquo; — reporting local ASN 65000, router ID 10.255.0.1, two peers 10.0.1.1 (AS 65101) and 10.0.2.1 (AS 65102) both Established with 1 prefix each; the right panel lists the six arista_ tools" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI. The agent&amp;rsquo;s whole toolset is visible on the right — six &lt;code>arista_*&lt;/code> reads, nothing else. It reports both eBGP sessions &lt;strong>Established&lt;/strong>, one prefix each, and closes with an operator-grade one-liner.&lt;/em>&lt;/p>
&lt;p>The number that matters isn&amp;rsquo;t the answer — it&amp;rsquo;s that the answer is &lt;strong>true&lt;/strong>. Here is the raw &lt;code>show ip bgp summary&lt;/code> from spine1 itself:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-bgp-summary.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-bgp-summary.png" alt="spine1 terminal: show ip bgp summary for VRF default, router identifier 10.255.0.1 local AS 65000, neighbors leaf1 10.0.1.1 AS 65101 and leaf2 10.0.2.1 AS 65102 both in Estab state with PfxRcd 1" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Ground truth from the switch: &lt;code>10.0.1.1&lt;/code> (AS 65101) and &lt;code>10.0.2.1&lt;/code> (AS 65102), both &lt;code>Estab&lt;/code>, &lt;code>PfxRcd 1&lt;/code>.&lt;/em>&lt;/p>
&lt;p>Line them up. Router ID &lt;code>10.255.0.1&lt;/code>, AS &lt;code>65000&lt;/code>, peers &lt;code>65101&lt;/code> and &lt;code>65102&lt;/code>, both Established, one prefix each — &lt;strong>the chat matches the CLI exactly&lt;/strong>. That&amp;rsquo;s the skill rule &amp;ldquo;never invent BGP state&amp;rdquo; paying off: the agent read the real device and reported it, nothing more. An answer you can trust is worth more than a clever one.&lt;/p>
&lt;h2 id="why-this-is-the-art-of-the-possible">Why this is the art of the possible&lt;/h2>
&lt;p>Step back from Arista and the pattern is the real story. The Arista operator is one of a &lt;em>family&lt;/em> of sibling agents in the same repo, each built the identical way — gVisor sandbox, Vault-backed secrets, read-only tools, skills in the system message — and each pointed at a different corner of the enterprise:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>SandboxAgent&lt;/th>
&lt;th>Operates&lt;/th>
&lt;th>Read-only reach&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>aws-budget&lt;/code>&lt;/td>
&lt;td>AWS account&lt;/td>
&lt;td>Cost Explorer, EC2/RDS/quota describes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fortigate&lt;/code>&lt;/td>
&lt;td>A FortiGate firewall&lt;/td>
&lt;td>Policy and status reads&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5-bigip&lt;/code>&lt;/td>
&lt;td>An F5 BIG-IP&lt;/td>
&lt;td>LTM/virtual-server state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>service-now&lt;/code>&lt;/td>
&lt;td>ServiceNow&lt;/td>
&lt;td>Ticket and CMDB reads&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp&lt;/code>&lt;/td>
&lt;td>A GCP project&lt;/td>
&lt;td>Billing and inventory&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>arista-ceos&lt;/code>&lt;/strong>&lt;/td>
&lt;td>&lt;strong>A live Arista fabric&lt;/strong>&lt;/td>
&lt;td>&lt;strong>eAPI &lt;code>show&lt;/code> commands&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The governance is uniform; only the tools change. That&amp;rsquo;s the art of the possible: once you have a governed substrate, adding a safe operator for a new system is &lt;em>not&lt;/em> a security research project — it&amp;rsquo;s writing a handful of read-only tool wrappers and a page of skill rules. The hard part (isolation, secret handling, blast-radius control) is solved once, in the substrate, and inherited by everything.&lt;/p>
&lt;div class="mermaid">flowchart TB
 sub[&amp;#34;Governed Sandbox Substrate&amp;lt;br/&amp;gt;(gVisor · Vault · read-only tools · skills)&amp;#34;]
 a[&amp;#34;aws-budget&amp;#34;]
 f[&amp;#34;fortigate&amp;#34;]
 b[&amp;#34;f5-bigip&amp;#34;]
 s[&amp;#34;service-now&amp;#34;]
 ar[&amp;#34;arista-ceos&amp;#34;]
 sub --&amp;gt; a
 sub --&amp;gt; f
 sub --&amp;gt; b
 sub --&amp;gt; s
 sub --&amp;gt; ar
 style sub fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style ar fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style a fill:#FFFFFF,stroke:#17181C,color:#17181C
 style f fill:#FFFFFF,stroke:#17181C,color:#17181C
 style b fill:#FFFFFF,stroke:#17181C,color:#17181C
 style s fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;h2 id="being-honest-about-the-edges">Being honest about the edges&lt;/h2>
&lt;p>This is v1, and the demo says so plainly:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Read-only by design.&lt;/strong> There are no write tools, on purpose. The day a &lt;code>configure&lt;/code>-class tool is added, it belongs behind human-in-the-loop approval, not in the same catalog as the reads.&lt;/li>
&lt;li>&lt;strong>eBGP only.&lt;/strong> No EVPN, VXLAN, or MPLS in the fabric yet — the underlay is kept boring so the agent&amp;rsquo;s reads are checkable.&lt;/li>
&lt;li>&lt;strong>Lab shortcuts are labeled.&lt;/strong> eAPI TLS verification is off &lt;em>for the lab&lt;/em>; the cEOS image is Arista-licensed and never vendored into git; the kagent UI is LAN-only. None of these are production claims.&lt;/li>
&lt;li>&lt;strong>The first boot failed honestly.&lt;/strong> cEOS died on inotify exhaustion (&lt;code>Too many open files&lt;/code>) until the host &lt;code>fs.inotify&lt;/code> limits were raised — a real operational wrinkle, recorded rather than airbrushed.&lt;/li>
&lt;/ul>
&lt;h2 id="the-takeaway-governed-autonomy">The takeaway: governed autonomy&lt;/h2>
&lt;p>The lesson of the Arista agent isn&amp;rsquo;t that the model is smart. It&amp;rsquo;s that the model is &lt;strong>contained&lt;/strong> — and containment is what makes the smartness usable. Skills say how to behave. A curated catalog removes the verbs for harm. A gVisor sandbox, Vault-held secrets, and a node allowlist mean even a fully hijacked session runs out of road before it reaches anything it could break.&lt;/p>
&lt;p>That combination is what lets you do the thing the network engineer&amp;rsquo;s instinct said you never could: put an agent in front of live infrastructure. Not because you stopped worrying about what an AI might do — but because you built the cage first, and the cage is the product.&lt;/p>
&lt;p>Governed autonomy is the unlock. The art of the possible is everything you can safely point it at next.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The Arista fabric, skills, MCP tool map, and the live captures in this article live in &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/arista-ceos-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / arista-ceos-sandbox-agent&lt;/a>, with the SandboxAgent wiring documented in &lt;a href="https://github.com/sebbycorp/k8s-viper/blob/main/docs/arista-ceos-agent.md">k8s-viper / docs/arista-ceos-agent.md&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>Ask a network engineer if they&amp;rsquo;d let an AI run commands on their production fabric and you&amp;rsquo;ll get a very short answer. The instinct is correct: a model that can &lt;code>configure&lt;/code>, &lt;code>clear ip bgp&lt;/code>, or &lt;code>shutdown&lt;/code> an interface is a model that can take down a data center between two tokens. For years that instinct has kept agents &lt;em>out&lt;/em> of the systems where they&amp;rsquo;d be most useful.&lt;/p>
&lt;p>But the instinct is aimed at the wrong thing. The danger was never &amp;ldquo;an AI touches the network.&amp;rdquo; The danger is &lt;strong>an AI with an ungoverned reach into the network&lt;/strong>. Remove the reach — cage it, scope it, watch it — and the same agent becomes something you&amp;rsquo;d actually want on call: a tireless read-only operator that can answer &amp;ldquo;is the fabric healthy?&amp;rdquo; at 3 a.m. without ever being able to break it.&lt;/p>
&lt;p>This article is about that cage, and the art of what becomes possible once you have one. The vehicle is a real &lt;a href="https://kagent.dev">kagent&lt;/a> demo — a &lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong> that operates a live 3-node &lt;strong>Arista cEOS&lt;/strong> fabric — and the point it proves generalizes far past networking: with a governed sandbox substrate, you can safely put an agent in front of almost anything.&lt;/p>
&lt;p>&lt;em>(If you want the foundations of the substrate itself — gVisor per-session isolation, snapshot/restore — start with &lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why Secure Sandbox Substrates Are the Future of AI Agents&lt;/a>. This piece builds on it and focuses on &lt;strong>governance&lt;/strong>: skills, tool catalogs, and blast-radius control.)&lt;/em>&lt;/p>
&lt;h2 id="this-time-the-agent-is-the-box">This time, the agent &lt;em>is&lt;/em> the box&lt;/h2>
&lt;p>Most agent demos talk to a system that already exists — a cloud API, a SaaS backend. This one is different: the thing being operated is a &lt;strong>live network fabric&lt;/strong> stood up just for the agent. Three Arista cEOS switches — &lt;code>spine1&lt;/code>, &lt;code>leaf1&lt;/code>, &lt;code>leaf2&lt;/code> — wired into a small Clos-ish underlay with Containerlab, running real EOS and real eBGP.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/clab-inspect.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/clab-inspect.png" alt="containerlab inspect showing three running nodes — clab-arista-ceos-leaf1, leaf2, and spine1 — all image ceos:4.33.9M on the 172.20.20.0/24 management network" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live on Viper, 2026-08-17. Three &lt;code>ceos:4.33.9M&lt;/code> nodes, &lt;code>running&lt;/code>, on the &lt;code>arista-ceos&lt;/code> management network.&lt;/em>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-show-version.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-show-version.png" alt="spine1 show version — Arista cEOSLab, software image version 4.33.9M-49063934.4339M, architecture x86_64, kernel 7.0.0-28-generic" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Real EOS, not a mock: spine1 running &lt;code>4.33.9M&lt;/code> on x86_64.&lt;/em>&lt;/p>
&lt;p>The topology is deliberately boring — boring is checkable. eBGP on point-to-point &lt;code>/31&lt;/code>s, one AS per switch, a loopback each. No EVPN, no MPLS, nothing an agent could misread.&lt;/p>
&lt;div class="mermaid">flowchart TB
 subgraph clab[&amp;#34;Containerlab fabric: arista-ceos&amp;#34;]
 spine1[&amp;#34;spine1&amp;lt;br/&amp;gt;AS 65000&amp;lt;br/&amp;gt;Lo0 10.255.0.1/32&amp;#34;]
 leaf1[&amp;#34;leaf1&amp;lt;br/&amp;gt;AS 65101&amp;lt;br/&amp;gt;Lo0 10.255.0.11/32&amp;#34;]
 leaf2[&amp;#34;leaf2&amp;lt;br/&amp;gt;AS 65102&amp;lt;br/&amp;gt;Lo0 10.255.0.12/32&amp;#34;]
 spine1 --&amp;gt;|&amp;#34;Ethernet1&amp;lt;br/&amp;gt;10.0.1.0/31&amp;#34;| leaf1
 spine1 --&amp;gt;|&amp;#34;Ethernet2&amp;lt;br/&amp;gt;10.0.2.0/31&amp;#34;| leaf2
 end
 style spine1 fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style leaf1 fill:#F1EFE9,stroke:#17181C,color:#17181C
 style leaf2 fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Now the interesting part: a kagent &lt;code>SandboxAgent&lt;/code> named &lt;code>arista-ceos&lt;/code> sits in front of this fabric as a &lt;strong>read-only network operator&lt;/strong>. It runs each conversation inside a gVisor actor, reaches the switches through a FastMCP server that speaks the EOS &lt;strong>eAPI&lt;/strong>, and answers questions in plain English. And — this is the whole game — it is governed on three independent layers so that &amp;ldquo;operator&amp;rdquo; can never quietly become &amp;ldquo;attacker.&amp;rdquo;&lt;/p>
&lt;h2 id="governance-layer-1--skills-that-encode-how-to-behave">Governance layer 1 — skills that encode &lt;em>how to behave&lt;/em>&lt;/h2>
&lt;p>An agent&amp;rsquo;s tools tell it &lt;em>what it can do&lt;/em>. Its &lt;strong>skills&lt;/strong> tell it &lt;em>how it should act&lt;/em>. In kagent, skills are markdown that gets folded into the agent&amp;rsquo;s &lt;code>systemMessage&lt;/code> (kagent &lt;code>0.10.0-rc2&lt;/code> deliberately rejects a &lt;code>spec.skills&lt;/code> field on sandbox agents, so the instructions ride in the prompt). That markdown is where operational judgment — and hard guardrails — live.&lt;/p>
&lt;p>The Arista agent ships three skills — &lt;code>fabric&lt;/code>, &lt;code>routing&lt;/code>, and &lt;code>executive-brief&lt;/code> — and every one of them opens with the same standing rules. They read like a runbook written by someone who&amp;rsquo;s been burned:&lt;/p>
&lt;blockquote>
&lt;ul>
&lt;li>&lt;strong>Read-only.&lt;/strong> No &lt;code>configure&lt;/code>, no &lt;code>write&lt;/code>, no neighbor shutdown, no image upgrade.&lt;/li>
&lt;li>&lt;strong>Never invent&lt;/strong> BGP state, prefixes, LLDP neighbors, or ping results. If eAPI fails or returns empty, say so.&lt;/li>
&lt;li>&lt;strong>Never print&lt;/strong> eAPI passwords, basic-auth headers, or Vault tokens.&lt;/li>
&lt;li>No generic CLI dump of the full running-config — it can include the lab AAA line. Prefer the named tools.&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>Notice what these rules are doing. &amp;ldquo;Never invent BGP state&amp;rdquo; turns the model&amp;rsquo;s most dangerous failure mode — a confident hallucination — into a contract: &lt;em>if the tool didn&amp;rsquo;t return it, don&amp;rsquo;t say it&lt;/em>. &amp;ldquo;Never print passwords&amp;rdquo; is a data-exfiltration guard written in English. &amp;ldquo;Prefer the named tools over a raw config dump&amp;rdquo; keeps secrets out of the transcript by construction. The skill even dictates the &lt;strong>shape&lt;/strong> of a good answer — lead with one sentence of fabric health, then a short table — so the agent is useful &lt;em>and&lt;/em> predictable.&lt;/p>
&lt;p>Skills are governance you can read, diff, and review in a pull request. That&amp;rsquo;s a very different thing from hoping a general-purpose model behaves.&lt;/p>
&lt;h2 id="governance-layer-2--a-curated-read-only-tool-catalog">Governance layer 2 — a curated, read-only tool catalog&lt;/h2>
&lt;p>Skills are the soft boundary. The &lt;strong>tool catalog&lt;/strong> is the hard one. The agent&amp;rsquo;s entire vocabulary for touching the fabric is six eAPI wrappers, and every one of them is a &lt;code>show&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>EOS command&lt;/th>
&lt;th>Scope&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>arista_inventory&lt;/code>&lt;/td>
&lt;td>&lt;code>show version&lt;/code>&lt;/td>
&lt;td>all nodes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_bgp_summary&lt;/code>&lt;/td>
&lt;td>&lt;code>show ip bgp summary&lt;/code>&lt;/td>
&lt;td>one or all nodes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_interfaces&lt;/code>&lt;/td>
&lt;td>&lt;code>show interfaces&lt;/code>&lt;/td>
&lt;td>single node&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_lldp_neighbors&lt;/code>&lt;/td>
&lt;td>&lt;code>show lldp neighbors&lt;/code>&lt;/td>
&lt;td>one or all nodes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_routes&lt;/code>&lt;/td>
&lt;td>&lt;code>show ip route [prefix]&lt;/code>&lt;/td>
&lt;td>single node, optional filter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>arista_health&lt;/code>&lt;/td>
&lt;td>composed summary&lt;/td>
&lt;td>all nodes, concise&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is &lt;strong>no&lt;/strong> &lt;code>configure&lt;/code>, no &lt;code>clear&lt;/code>, no &lt;code>reload&lt;/code>, no generic &amp;ldquo;run any EOS command&amp;rdquo; escape hatch. This matters more than any prompt rule, because it&amp;rsquo;s not advisory — it&amp;rsquo;s the shape of the API. Even a perfectly jailbroken model, convinced by some injected instruction that it must reset a BGP session, has no verb to do it. The catalog simply doesn&amp;rsquo;t contain destruction.&lt;/p>
&lt;p>This is the same design decision behind every agent in the family: expose &lt;em>Describe / Get / View / Show&lt;/em>, and nothing that mutates. You lose nothing an operator needs for triage, and you remove the entire class of &amp;ldquo;the agent changed something&amp;rdquo; incidents.&lt;/p>
&lt;h2 id="governance-layer-3--the-sandbox-the-secrets-and-the-allowlist">Governance layer 3 — the sandbox, the secrets, and the allowlist&lt;/h2>
&lt;p>The third wall is the runtime itself.&lt;/p>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Operator chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;(the session)&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;arista-ceos-mcp&amp;#34;]
 mcp[&amp;#34;arista-ceos-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 fabric[&amp;#34;cEOS eAPI&amp;lt;br/&amp;gt;spine1 · leaf1 · leaf2&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/arista-ceos&amp;#34;]
 eso[&amp;#34;External Secrets&amp;lt;br/&amp;gt;Operator&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; fabric
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style vault fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Three things are happening on that diagram, each a control:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>gVisor isolation.&lt;/strong> The model session runs in a gVisor actor — a user-space kernel between the agent and the host. Untrusted tool output (and any injection riding in it) never gets to talk to the node&amp;rsquo;s real kernel.&lt;/li>
&lt;li>&lt;strong>Secrets the session never holds.&lt;/strong> The eAPI username and password live in &lt;strong>Vault&lt;/strong> at &lt;code>secret/platform/arista-ceos&lt;/code> and are synced by External Secrets Operator into the &lt;strong>MCP pod only&lt;/strong> — as &lt;code>username&lt;/code>, &lt;code>password&lt;/code>, and a &lt;code>hosts_json&lt;/code> map of allowlisted node → eAPI URL. The gVisor actor calls a tool; the tool holds the credential; the session never sees it.&lt;/li>
&lt;li>&lt;strong>An explicit node allowlist.&lt;/strong> The MCP server is pinned to &lt;code>ARISTA_ALLOWED_NODES=spine1,leaf1,leaf2&lt;/code>. Ask it about a switch that isn&amp;rsquo;t on the list and there&amp;rsquo;s nowhere for the request to go. (TLS verification is disabled &lt;em>for this lab only&lt;/em> — a documented lab shortcut, not a production posture.)&lt;/li>
&lt;/ul>
&lt;p>Stack the three layers and you get genuine defense in depth. A prompt injection has to get past the &lt;strong>skill rules&lt;/strong>, then find a destructive &lt;strong>tool that doesn&amp;rsquo;t exist&lt;/strong>, then escape a &lt;strong>gVisor sandbox&lt;/strong>, then reach a credential that &lt;strong>isn&amp;rsquo;t in the session&lt;/strong>, to touch a node that &lt;strong>isn&amp;rsquo;t on the allowlist&lt;/strong>. Each wall is boring on its own. Together they turn &amp;ldquo;an AI on the network&amp;rdquo; from reckless into routine.&lt;/p>
&lt;h2 id="watch-it-actually-work">Watch it actually work&lt;/h2>
&lt;p>Here&amp;rsquo;s the agent answering a real operator question — &lt;em>&amp;ldquo;What is the BGP summary on spine1?&amp;rdquo;&lt;/em> — with exactly &lt;strong>one&lt;/strong> tool call to &lt;code>arista_bgp_summary&lt;/code>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/kagent-arista-bgp-chat.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/kagent-arista-bgp-chat.png" alt="kagent chat: the arista-ceos agent answers &amp;amp;lsquo;What is the BGP summary on spine1?&amp;amp;rsquo; — reporting local ASN 65000, router ID 10.255.0.1, two peers 10.0.1.1 (AS 65101) and 10.0.2.1 (AS 65102) both Established with 1 prefix each; the right panel lists the six arista_ tools" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI. The agent&amp;rsquo;s whole toolset is visible on the right — six &lt;code>arista_*&lt;/code> reads, nothing else. It reports both eBGP sessions &lt;strong>Established&lt;/strong>, one prefix each, and closes with an operator-grade one-liner.&lt;/em>&lt;/p>
&lt;p>The number that matters isn&amp;rsquo;t the answer — it&amp;rsquo;s that the answer is &lt;strong>true&lt;/strong>. Here is the raw &lt;code>show ip bgp summary&lt;/code> from spine1 itself:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-bgp-summary.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-18-governed-sandbox-agents-kagent-arista/spine1-bgp-summary.png" alt="spine1 terminal: show ip bgp summary for VRF default, router identifier 10.255.0.1 local AS 65000, neighbors leaf1 10.0.1.1 AS 65101 and leaf2 10.0.2.1 AS 65102 both in Estab state with PfxRcd 1" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Ground truth from the switch: &lt;code>10.0.1.1&lt;/code> (AS 65101) and &lt;code>10.0.2.1&lt;/code> (AS 65102), both &lt;code>Estab&lt;/code>, &lt;code>PfxRcd 1&lt;/code>.&lt;/em>&lt;/p>
&lt;p>Line them up. Router ID &lt;code>10.255.0.1&lt;/code>, AS &lt;code>65000&lt;/code>, peers &lt;code>65101&lt;/code> and &lt;code>65102&lt;/code>, both Established, one prefix each — &lt;strong>the chat matches the CLI exactly&lt;/strong>. That&amp;rsquo;s the skill rule &amp;ldquo;never invent BGP state&amp;rdquo; paying off: the agent read the real device and reported it, nothing more. An answer you can trust is worth more than a clever one.&lt;/p>
&lt;h2 id="why-this-is-the-art-of-the-possible">Why this is the art of the possible&lt;/h2>
&lt;p>Step back from Arista and the pattern is the real story. The Arista operator is one of a &lt;em>family&lt;/em> of sibling agents in the same repo, each built the identical way — gVisor sandbox, Vault-backed secrets, read-only tools, skills in the system message — and each pointed at a different corner of the enterprise:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>SandboxAgent&lt;/th>
&lt;th>Operates&lt;/th>
&lt;th>Read-only reach&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>aws-budget&lt;/code>&lt;/td>
&lt;td>AWS account&lt;/td>
&lt;td>Cost Explorer, EC2/RDS/quota describes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fortigate&lt;/code>&lt;/td>
&lt;td>A FortiGate firewall&lt;/td>
&lt;td>Policy and status reads&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5-bigip&lt;/code>&lt;/td>
&lt;td>An F5 BIG-IP&lt;/td>
&lt;td>LTM/virtual-server state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>service-now&lt;/code>&lt;/td>
&lt;td>ServiceNow&lt;/td>
&lt;td>Ticket and CMDB reads&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp&lt;/code>&lt;/td>
&lt;td>A GCP project&lt;/td>
&lt;td>Billing and inventory&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>arista-ceos&lt;/code>&lt;/strong>&lt;/td>
&lt;td>&lt;strong>A live Arista fabric&lt;/strong>&lt;/td>
&lt;td>&lt;strong>eAPI &lt;code>show&lt;/code> commands&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The governance is uniform; only the tools change. That&amp;rsquo;s the art of the possible: once you have a governed substrate, adding a safe operator for a new system is &lt;em>not&lt;/em> a security research project — it&amp;rsquo;s writing a handful of read-only tool wrappers and a page of skill rules. The hard part (isolation, secret handling, blast-radius control) is solved once, in the substrate, and inherited by everything.&lt;/p>
&lt;div class="mermaid">flowchart TB
 sub[&amp;#34;Governed Sandbox Substrate&amp;lt;br/&amp;gt;(gVisor · Vault · read-only tools · skills)&amp;#34;]
 a[&amp;#34;aws-budget&amp;#34;]
 f[&amp;#34;fortigate&amp;#34;]
 b[&amp;#34;f5-bigip&amp;#34;]
 s[&amp;#34;service-now&amp;#34;]
 ar[&amp;#34;arista-ceos&amp;#34;]
 sub --&amp;gt; a
 sub --&amp;gt; f
 sub --&amp;gt; b
 sub --&amp;gt; s
 sub --&amp;gt; ar
 style sub fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style ar fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style a fill:#FFFFFF,stroke:#17181C,color:#17181C
 style f fill:#FFFFFF,stroke:#17181C,color:#17181C
 style b fill:#FFFFFF,stroke:#17181C,color:#17181C
 style s fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;h2 id="being-honest-about-the-edges">Being honest about the edges&lt;/h2>
&lt;p>This is v1, and the demo says so plainly:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Read-only by design.&lt;/strong> There are no write tools, on purpose. The day a &lt;code>configure&lt;/code>-class tool is added, it belongs behind human-in-the-loop approval, not in the same catalog as the reads.&lt;/li>
&lt;li>&lt;strong>eBGP only.&lt;/strong> No EVPN, VXLAN, or MPLS in the fabric yet — the underlay is kept boring so the agent&amp;rsquo;s reads are checkable.&lt;/li>
&lt;li>&lt;strong>Lab shortcuts are labeled.&lt;/strong> eAPI TLS verification is off &lt;em>for the lab&lt;/em>; the cEOS image is Arista-licensed and never vendored into git; the kagent UI is LAN-only. None of these are production claims.&lt;/li>
&lt;li>&lt;strong>The first boot failed honestly.&lt;/strong> cEOS died on inotify exhaustion (&lt;code>Too many open files&lt;/code>) until the host &lt;code>fs.inotify&lt;/code> limits were raised — a real operational wrinkle, recorded rather than airbrushed.&lt;/li>
&lt;/ul>
&lt;h2 id="the-takeaway-governed-autonomy">The takeaway: governed autonomy&lt;/h2>
&lt;p>The lesson of the Arista agent isn&amp;rsquo;t that the model is smart. It&amp;rsquo;s that the model is &lt;strong>contained&lt;/strong> — and containment is what makes the smartness usable. Skills say how to behave. A curated catalog removes the verbs for harm. A gVisor sandbox, Vault-held secrets, and a node allowlist mean even a fully hijacked session runs out of road before it reaches anything it could break.&lt;/p>
&lt;p>That combination is what lets you do the thing the network engineer&amp;rsquo;s instinct said you never could: put an agent in front of live infrastructure. Not because you stopped worrying about what an AI might do — but because you built the cage first, and the cage is the product.&lt;/p>
&lt;p>Governed autonomy is the unlock. The art of the possible is everything you can safely point it at next.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The Arista fabric, skills, MCP tool map, and the live captures in this article live in &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/arista-ceos-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / arista-ceos-sandbox-agent&lt;/a>, with the SandboxAgent wiring documented in &lt;a href="https://github.com/sebbycorp/k8s-viper/blob/main/docs/arista-ceos-agent.md">k8s-viper / docs/arista-ceos-agent.md&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>How To: Build an AWS Budget SandboxAgent on kagent + Agent Substrate</title><link>https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/</link><pubDate>Mon, 17 Aug 2026 09:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/</guid><description>&lt;p>I already wrote the &lt;em>why&lt;/em> — &lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">why secure sandbox substrates are the future&lt;/a>. This one is the &lt;em>how&lt;/em>. Same agent, but from the build side: every object you apply, where the AWS key lives, and the one version pin that will waste your afternoon if you get it wrong.&lt;/p>
&lt;p>The goal is a single question, answered honestly:&lt;/p>
&lt;blockquote>
&lt;p>What&amp;rsquo;s our us-east-2 spend this month, and are we over capacity?&lt;/p>
&lt;/blockquote>
&lt;p>Not &amp;ldquo;roughly.&amp;rdquo; Not an estimate. Real &lt;code>ce:GetCostAndUsage&lt;/code> numbers, or an explicit &lt;em>denied&lt;/em>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" alt="kagent UI Agents grid showing three SandboxAgent cards — aws-budget, fortigate, and hello-substrate — each running OpenAI gpt-5.5" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three &lt;strong>SandboxAgent&lt;/strong> cards, not plain Agents. &lt;code>aws-budget&lt;/code> is described as &amp;ldquo;Executive AWS budget and capacity assistant for us-east-2 (gVisor).&amp;rdquo;&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Kubernetes Deployment. Always on, container isolation, same as any other pod. That is completely fine for a cluster helper that reads pod logs.&lt;/p>
&lt;p>This agent talks to your AWS bill. The model gets a filesystem, memory, and a live network for the whole conversation, and it chews on tool output it did not author. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> — a user-space kernel between the session and your k3s host.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Plain kagent &lt;code>Agent&lt;/code>&lt;/th>
&lt;th>&lt;code>SandboxAgent&lt;/code> (Substrate)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Kubernetes shape&lt;/td>
&lt;td>Deployment, always-on pod&lt;/td>
&lt;td>Actor on WorkerPool &lt;code>kagent-default&lt;/code>, booted per session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Isolation&lt;/td>
&lt;td>Container, shared host kernel&lt;/td>
&lt;td>&lt;strong>gVisor&lt;/strong> user-space kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Idle cost&lt;/td>
&lt;td>A pod per conversation&lt;/td>
&lt;td>Snapshot (zstd), worker freed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Resume&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>Restore the exact session&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Honest rule of thumb: if all you need is a Python container with &lt;code>boto3&lt;/code> and no snapshot lifecycle, use a Deployment. The moment the session reads your bill, take the wall.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Executive chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;lt;br/&amp;gt;/api/a2a-sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;aws-budget-mcp&amp;#34;]
 mcp[&amp;#34;aws-budget-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 aws[&amp;#34;AWS us-east-2&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/aws-budget&amp;#34;]
 eso[&amp;#34;External Secrets Operator&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; aws
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>Read that bottom row carefully. Vault and ESO feed &lt;strong>the MCP pod&lt;/strong>, not the actor. The gVisor session — the part running the model — never holds an AWS access key. It calls a tool; the tool makes the API call on the far side of the wall.&lt;/p>
&lt;h2 id="the-pins-that-matter-do-not-bump">The pins that matter (do not bump)&lt;/h2>
&lt;p>This is the part I would tattoo on the runbook.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (my lab: gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Region&lt;/td>
&lt;td>&lt;strong>us-east-2&lt;/strong> only&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>kagent rc2 &lt;strong>always&lt;/strong> writes an &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code> (for &lt;code>KAGENT_CONFIG_JSON&lt;/code>, &lt;code>KAGENT_AGENT_CARD_JSON&lt;/code>, &lt;code>KAGENT_SRT_SETTINGS_JSON&lt;/code>, &lt;code>OPENAI_API_KEY&lt;/code>).&lt;/p>
&lt;p>Substrate &lt;strong>0.0.9&lt;/strong> CRDs accept that shape. Substrate &lt;strong>0.0.12&lt;/strong> removed &lt;code>valueFrom&lt;/code> and moved the pause image into &lt;code>SandboxConfig&lt;/code>. Point rc2 at 0.0.12 and the apiserver rejects the object — you get &lt;code>Ready=False&lt;/code>, &lt;code>ActorTemplateNotFound&lt;/code>, or &lt;code>spec.containers[0].env[…].value: Required value&lt;/code>.&lt;/p>
&lt;p>The trap: the first time your agent is not Ready, your instinct is to upgrade. &lt;strong>Don&amp;rsquo;t.&lt;/strong> A pin mismatch is a CRD problem, not a staleness problem. And don&amp;rsquo;t &amp;ldquo;fix&amp;rdquo; it by flattening the env into literal values either.&lt;/p>
&lt;h2 id="snapshots-today-rustfs-and-omit-snapshotsconfig">Snapshots today: rustfs, and omit &lt;code>snapshotsConfig&lt;/code>&lt;/h2>
&lt;p>Substrate checkpoints actor RAM and filesystem to object storage. On my Viper lab that storage is &lt;strong>in-cluster rustfs&lt;/strong>, not Google Cloud:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Live today&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>atelet 0.0.9&lt;/td>
&lt;td>&lt;code>ATE_STORAGE_BACKEND=s3&lt;/code> → &lt;code>http://rustfs.ate-system.svc:9000&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bucket&lt;/td>
&lt;td>&lt;code>ate-snapshots&lt;/code> (1Gi PVC)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SandboxAgent&lt;/td>
&lt;td>&lt;strong>omits&lt;/strong> &lt;code>snapshotsConfig&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ActorTemplate location&lt;/td>
&lt;td>&lt;code>gs://ate-snapshots/kagent/aws-budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>gs://&lt;/code> scheme there is a &lt;strong>URI prefix only&lt;/strong>. The bytes are on rustfs. I have a real GCS bucket (&lt;code>gs://viper-kagent-ate-snapshots&lt;/code>, project &lt;code>viper-kagent&lt;/code>, org maniak.io) reserved for a later cluster-wide atelet cutover, and I deliberately did &lt;strong>not&lt;/strong> point this agent at it — atelet would go looking for that bucket &lt;em>on rustfs&lt;/em>, not find it, and the golden snapshot would fail. No &lt;code>ignoreDifferences&lt;/code>. Stay on rustfs.&lt;/p>
&lt;h2 id="step-1--a-dedicated-iam-identity">Step 1 — a dedicated IAM identity&lt;/h2>
&lt;p>AWS console, region us-east-2 → IAM → create user &lt;code>aws-budget-agent&lt;/code>, no console password → attach a customer-managed policy → create an access key (&amp;ldquo;application running outside AWS&amp;rdquo;).&lt;/p>
&lt;p>The policy is &lt;code>AwsBudgetAgentReadOnly&lt;/code>: &lt;code>ce:Get*&lt;/code>, &lt;code>budgets:View*&lt;/code>, &lt;code>ec2:Describe*&lt;/code> and siblings, conditioned to &lt;code>us-east-2&lt;/code> wherever IAM allows a region condition. No &lt;code>iam:Create*&lt;/code>. No &lt;code>ec2:Terminate*&lt;/code>. No &lt;code>budgets:Delete*&lt;/code>. No &lt;code>s3:*&lt;/code> on your data.&lt;/p>
&lt;p>Why a dedicated identity: the agent is read-mostly and it must not borrow a human admin key. If someone talks the model into something clever, the ceiling is &amp;ldquo;read one region&amp;rsquo;s billing metadata.&amp;rdquo;&lt;/p>
&lt;p>Two housekeeping notes:&lt;/p>
&lt;ul>
&lt;li>Enable &lt;strong>Cost Explorer&lt;/strong> in the billing console if the account has never used it. Otherwise &lt;code>aws_cost_*&lt;/code> fails honestly — and an honest failure is success, not a reason to fake &lt;code>$0&lt;/code>.&lt;/li>
&lt;li>Compute Optimizer / CE rightsizing enrollment is optional. Skip it and &lt;code>aws_rightsizing_hints&lt;/code> must say &amp;ldquo;not available.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;h2 id="step-2--keys-into-vault-never-git">Step 2 — keys into Vault, never git&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -it k3s-viper kubectl -n vault &lt;span class="nb">exec&lt;/span> -i vault-0 -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> vault kv put secret/platform/aws-budget &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">access_key_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;&amp;lt;paste&amp;gt;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">secret_access_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;&amp;lt;paste&amp;gt;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">region&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;us-east-2&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then let ESO do the sync:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent get externalsecret aws-budget-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You want &lt;code>STATUS: SecretSynced&lt;/code>. Git only ever holds the &lt;code>ExternalSecret&lt;/code> &lt;strong>mapping&lt;/strong> — path names and key names. Never values. And please don&amp;rsquo;t &lt;code>kubectl get secret -o yaml&lt;/code> and paste the result into Slack.&lt;/p>
&lt;h2 id="step-3--build-and-import-the-mcp-image">Step 3 — build and import the MCP image&lt;/h2>
&lt;p>Dockerized k3s cannot see host Docker images, and there is no registry pull for &lt;code>aws-budget-mcp:dev&lt;/code>, so you import it into containerd:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./aws-sandbox-agent/scripts/02-build-import-mcp.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That builds the image and runs &lt;code>ctr images import&lt;/code>. The Deployment uses &lt;code>imagePullPolicy: IfNotPresent&lt;/code> on purpose. &lt;code>ImagePullBackOff&lt;/code> means the import didn&amp;rsquo;t land or your tag doesn&amp;rsquo;t match.&lt;/p>
&lt;h2 id="step-4--apply-the-agent">Step 4 — apply the agent&lt;/h2>
&lt;p>Note the shape of this command. Kustomize runs on the &lt;strong>host&lt;/strong>, and the output is piped into k3s — not &lt;code>apply -k&lt;/code> against a host path the container can&amp;rsquo;t see:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl kustomize aws-sandbox-agent/k8s &lt;span class="p">|&lt;/span> docker &lt;span class="nb">exec&lt;/span> -i k3s-viper kubectl apply -f -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">externalsecret.external-secrets.io/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">deployment.apps/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">service/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">configmap/aws-budget-skills created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/aws-budget created
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One wrinkle worth knowing: kagent 0.10.0-rc2 &lt;strong>rejects &lt;code>spec.skills&lt;/code>&lt;/strong> on a &lt;code>SandboxAgent&lt;/code>, and there is no skill-mount into the gVisor session. So the skill text lives in ConfigMap &lt;code>aws-budget-skills&lt;/code> and gets inlined into &lt;code>systemMessage&lt;/code> via &lt;code>declarative.promptTemplate&lt;/code>. Keep &lt;code>skills/&lt;/code> and &lt;code>k8s/skills-configmap.yaml&lt;/code> in sync.&lt;/p>
&lt;h2 id="step-5--what-ready-actually-means">Step 5 — what &amp;ldquo;Ready&amp;rdquo; actually means&lt;/h2>
&lt;p>kagent does not start a Deployment of the LLM runtime. It creates an &lt;code>ActorTemplate&lt;/code> owned by the SandboxAgent. Substrate boots a &lt;strong>golden&lt;/strong> actor once, checkpoints it, and stores that checkpoint as &lt;code>status.goldenSnapshot&lt;/code>. New chats restore from that image.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>You see&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>No &lt;code>ActorTemplate&lt;/code>&lt;/td>
&lt;td>Wrong CRD pin (0.0.12) or the controller isn&amp;rsquo;t reconciling&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Template, no &lt;code>goldenSnapshot&lt;/code>&lt;/td>
&lt;td>First checkpoint running (60–90s is normal), or gVisor/atelet can&amp;rsquo;t write storage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Ready=True&lt;/code> + &lt;code>goldenSnapshot: gs://…&lt;/code>&lt;/td>
&lt;td>You can talk to it&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent get sandboxagents,remotemcpservers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent get actortemplates
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-tool-catalog--no-generic-shell">The tool catalog — no generic shell&lt;/h2>
&lt;p>Eleven tools, all Describe/Get/View/List. This is the single most important design decision in the demo, and it&amp;rsquo;s a design decision, not a config flag:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>AWS API (typical)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>aws_whoami&lt;/code>&lt;/td>
&lt;td>&lt;code>sts:GetCallerIdentity&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_month&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_by_service&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code> grouped by service&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_budgets&lt;/code>&lt;/td>
&lt;td>&lt;code>budgets:ViewBudget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_ec2_capacity&lt;/code>&lt;/td>
&lt;td>&lt;code>ec2:DescribeInstances&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_asg&lt;/code>&lt;/td>
&lt;td>&lt;code>autoscaling:Describe*&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_rds&lt;/code>&lt;/td>
&lt;td>&lt;code>rds:Describe*&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_ebs_summary&lt;/code>&lt;/td>
&lt;td>&lt;code>ec2:DescribeVolumes&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_service_quotas&lt;/code>&lt;/td>
&lt;td>&lt;code>servicequotas:GetServiceQuota&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_rightsizing_hints&lt;/code>&lt;/td>
&lt;td>CE rightsizing / Compute Optimizer&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_executive_brief&lt;/code>&lt;/td>
&lt;td>composes the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is &lt;strong>no&lt;/strong> &amp;ldquo;run any aws cli&amp;rdquo; tool. The agent has no verb for destruction. That boundary is enforced three independent times — in the tool code, in the IAM policy, and in the sandbox — so getting past one layer still lands you on the next.&lt;/p>
&lt;p>Small API detail that trips people up: Cost Explorer and Budgets live in &lt;strong>us-east-1&lt;/strong>. The tools still &lt;em>filter&lt;/em> results to us-east-2.&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>2026-08-16 on Viper, through &lt;code>/api/a2a-sandboxes/kagent/aws-budget&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Month-to-date spend&lt;/td>
&lt;td>&lt;strong>$0.67&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Budget&lt;/td>
&lt;td>&lt;strong>$4.13 / $100&lt;/strong> (4.13% used)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>EC2 / ASG / RDS / EBS&lt;/td>
&lt;td>&lt;strong>0 / 0 / 0 / 0&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Identity&lt;/td>
&lt;td>&lt;code>aws-budget-agent&lt;/code>, account 616973157416&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tool calls&lt;/td>
&lt;td>&lt;strong>10 / 10&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" alt="Live kagent chat: aws-budget reports us-east-2 MTD spend of $0.67, budget $4.13 of $100 used, and zero EC2/ASG/RDS/EBS capacity, with a full status table" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Ten of ten tool calls, every number sourced from a real AWS API.&lt;/em>&lt;/p>
&lt;p>The line I care about most is not the dollar figure. It&amp;rsquo;s this: the agent reported &amp;ldquo;Cost Explorer rightsizing API denied; Compute Optimizer not enrolled&amp;rdquo; instead of inventing a recommendation. An agent that fabricates a helpful-sounding number to fill a gap is an agent you cannot put in front of a budget.&lt;/p>
&lt;p>If yours &lt;em>does&lt;/em> invent spend, the tools didn&amp;rsquo;t run. Tell it &amp;ldquo;call the tools; do not estimate,&amp;rdquo; then check the MCP logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent logs deploy/aws-budget-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That process must never print the secret key.&lt;/p>
&lt;h2 id="proof-with-nothing-sensitive-on-screen">Proof, with nothing sensitive on screen&lt;/h2>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" alt="Terminal showing kubectl output: sandboxagent aws-budget READY True ACCEPTED True, remotemcpserver aws-budget-mcp STREAMABLE_HTTP, pod aws-budget-mcp 1/1 Running" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live capture, 2026-08-16. &lt;code>Ready=True&lt;/code>, MCP &lt;code>Accepted&lt;/code> with 11 tools, pod &lt;code>1/1 Running&lt;/code> — and deliberately no Vault tokens or AWS keys in frame.&lt;/em>&lt;/p>
&lt;p>And the whole turn as a short reel:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" alt="Animated reel of the aws-budget agent handling the spend-and-capacity question end to end" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>One tell that these are genuinely sandboxed sessions: the classic &lt;code>/api/a2a/kagent/aws-budget&lt;/code> endpoint &lt;strong>404s&lt;/strong>, because there is no &lt;code>Agent&lt;/code> CR at all. The UI talks to &lt;code>/api/a2a-sandboxes/kagent/aws-budget&lt;/code>, and the card wears a &lt;em>Sandbox: Agent Substrate&lt;/em> badge.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>The image must be imported on the k3s node (&lt;code>ctr images import&lt;/code>) before the MCP pod starts.&lt;/li>
&lt;li>The Vault path must exist first, or the ExternalSecret sits unsynced forever.&lt;/li>
&lt;li>Nested gVisor on dockerized k3s can still hit &lt;code>runsc&lt;/code> / seccomp / &lt;code>/dev/kvm&lt;/code> problems. That&amp;rsquo;s a worker-environment issue — not a reason to downgrade the agent to an unsandboxed Deployment.&lt;/li>
&lt;li>Snapshots are local rustfs. Cluster-wide object storage is future work.&lt;/li>
&lt;li>Never commit AWS or GCP secret &lt;strong>values&lt;/strong>. Manifests carry paths and key names only.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Full demo — manifests, the FastMCP server, IAM policy, runbooks, and the live report this draws on: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/aws-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / aws-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>I already wrote the &lt;em>why&lt;/em> — &lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">why secure sandbox substrates are the future&lt;/a>. This one is the &lt;em>how&lt;/em>. Same agent, but from the build side: every object you apply, where the AWS key lives, and the one version pin that will waste your afternoon if you get it wrong.&lt;/p>
&lt;p>The goal is a single question, answered honestly:&lt;/p>
&lt;blockquote>
&lt;p>What&amp;rsquo;s our us-east-2 spend this month, and are we over capacity?&lt;/p>
&lt;/blockquote>
&lt;p>Not &amp;ldquo;roughly.&amp;rdquo; Not an estimate. Real &lt;code>ce:GetCostAndUsage&lt;/code> numbers, or an explicit &lt;em>denied&lt;/em>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" alt="kagent UI Agents grid showing three SandboxAgent cards — aws-budget, fortigate, and hello-substrate — each running OpenAI gpt-5.5" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three &lt;strong>SandboxAgent&lt;/strong> cards, not plain Agents. &lt;code>aws-budget&lt;/code> is described as &amp;ldquo;Executive AWS budget and capacity assistant for us-east-2 (gVisor).&amp;rdquo;&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Kubernetes Deployment. Always on, container isolation, same as any other pod. That is completely fine for a cluster helper that reads pod logs.&lt;/p>
&lt;p>This agent talks to your AWS bill. The model gets a filesystem, memory, and a live network for the whole conversation, and it chews on tool output it did not author. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> — a user-space kernel between the session and your k3s host.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Plain kagent &lt;code>Agent&lt;/code>&lt;/th>
&lt;th>&lt;code>SandboxAgent&lt;/code> (Substrate)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Kubernetes shape&lt;/td>
&lt;td>Deployment, always-on pod&lt;/td>
&lt;td>Actor on WorkerPool &lt;code>kagent-default&lt;/code>, booted per session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Isolation&lt;/td>
&lt;td>Container, shared host kernel&lt;/td>
&lt;td>&lt;strong>gVisor&lt;/strong> user-space kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Idle cost&lt;/td>
&lt;td>A pod per conversation&lt;/td>
&lt;td>Snapshot (zstd), worker freed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Resume&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>Restore the exact session&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Honest rule of thumb: if all you need is a Python container with &lt;code>boto3&lt;/code> and no snapshot lifecycle, use a Deployment. The moment the session reads your bill, take the wall.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Executive chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;lt;br/&amp;gt;/api/a2a-sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;aws-budget-mcp&amp;#34;]
 mcp[&amp;#34;aws-budget-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 aws[&amp;#34;AWS us-east-2&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/aws-budget&amp;#34;]
 eso[&amp;#34;External Secrets Operator&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; aws
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>Read that bottom row carefully. Vault and ESO feed &lt;strong>the MCP pod&lt;/strong>, not the actor. The gVisor session — the part running the model — never holds an AWS access key. It calls a tool; the tool makes the API call on the far side of the wall.&lt;/p>
&lt;h2 id="the-pins-that-matter-do-not-bump">The pins that matter (do not bump)&lt;/h2>
&lt;p>This is the part I would tattoo on the runbook.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (my lab: gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Region&lt;/td>
&lt;td>&lt;strong>us-east-2&lt;/strong> only&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>kagent rc2 &lt;strong>always&lt;/strong> writes an &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code> (for &lt;code>KAGENT_CONFIG_JSON&lt;/code>, &lt;code>KAGENT_AGENT_CARD_JSON&lt;/code>, &lt;code>KAGENT_SRT_SETTINGS_JSON&lt;/code>, &lt;code>OPENAI_API_KEY&lt;/code>).&lt;/p>
&lt;p>Substrate &lt;strong>0.0.9&lt;/strong> CRDs accept that shape. Substrate &lt;strong>0.0.12&lt;/strong> removed &lt;code>valueFrom&lt;/code> and moved the pause image into &lt;code>SandboxConfig&lt;/code>. Point rc2 at 0.0.12 and the apiserver rejects the object — you get &lt;code>Ready=False&lt;/code>, &lt;code>ActorTemplateNotFound&lt;/code>, or &lt;code>spec.containers[0].env[…].value: Required value&lt;/code>.&lt;/p>
&lt;p>The trap: the first time your agent is not Ready, your instinct is to upgrade. &lt;strong>Don&amp;rsquo;t.&lt;/strong> A pin mismatch is a CRD problem, not a staleness problem. And don&amp;rsquo;t &amp;ldquo;fix&amp;rdquo; it by flattening the env into literal values either.&lt;/p>
&lt;h2 id="snapshots-today-rustfs-and-omit-snapshotsconfig">Snapshots today: rustfs, and omit &lt;code>snapshotsConfig&lt;/code>&lt;/h2>
&lt;p>Substrate checkpoints actor RAM and filesystem to object storage. On my Viper lab that storage is &lt;strong>in-cluster rustfs&lt;/strong>, not Google Cloud:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Live today&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>atelet 0.0.9&lt;/td>
&lt;td>&lt;code>ATE_STORAGE_BACKEND=s3&lt;/code> → &lt;code>http://rustfs.ate-system.svc:9000&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bucket&lt;/td>
&lt;td>&lt;code>ate-snapshots&lt;/code> (1Gi PVC)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SandboxAgent&lt;/td>
&lt;td>&lt;strong>omits&lt;/strong> &lt;code>snapshotsConfig&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ActorTemplate location&lt;/td>
&lt;td>&lt;code>gs://ate-snapshots/kagent/aws-budget&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>gs://&lt;/code> scheme there is a &lt;strong>URI prefix only&lt;/strong>. The bytes are on rustfs. I have a real GCS bucket (&lt;code>gs://viper-kagent-ate-snapshots&lt;/code>, project &lt;code>viper-kagent&lt;/code>, org maniak.io) reserved for a later cluster-wide atelet cutover, and I deliberately did &lt;strong>not&lt;/strong> point this agent at it — atelet would go looking for that bucket &lt;em>on rustfs&lt;/em>, not find it, and the golden snapshot would fail. No &lt;code>ignoreDifferences&lt;/code>. Stay on rustfs.&lt;/p>
&lt;h2 id="step-1--a-dedicated-iam-identity">Step 1 — a dedicated IAM identity&lt;/h2>
&lt;p>AWS console, region us-east-2 → IAM → create user &lt;code>aws-budget-agent&lt;/code>, no console password → attach a customer-managed policy → create an access key (&amp;ldquo;application running outside AWS&amp;rdquo;).&lt;/p>
&lt;p>The policy is &lt;code>AwsBudgetAgentReadOnly&lt;/code>: &lt;code>ce:Get*&lt;/code>, &lt;code>budgets:View*&lt;/code>, &lt;code>ec2:Describe*&lt;/code> and siblings, conditioned to &lt;code>us-east-2&lt;/code> wherever IAM allows a region condition. No &lt;code>iam:Create*&lt;/code>. No &lt;code>ec2:Terminate*&lt;/code>. No &lt;code>budgets:Delete*&lt;/code>. No &lt;code>s3:*&lt;/code> on your data.&lt;/p>
&lt;p>Why a dedicated identity: the agent is read-mostly and it must not borrow a human admin key. If someone talks the model into something clever, the ceiling is &amp;ldquo;read one region&amp;rsquo;s billing metadata.&amp;rdquo;&lt;/p>
&lt;p>Two housekeeping notes:&lt;/p>
&lt;ul>
&lt;li>Enable &lt;strong>Cost Explorer&lt;/strong> in the billing console if the account has never used it. Otherwise &lt;code>aws_cost_*&lt;/code> fails honestly — and an honest failure is success, not a reason to fake &lt;code>$0&lt;/code>.&lt;/li>
&lt;li>Compute Optimizer / CE rightsizing enrollment is optional. Skip it and &lt;code>aws_rightsizing_hints&lt;/code> must say &amp;ldquo;not available.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;h2 id="step-2--keys-into-vault-never-git">Step 2 — keys into Vault, never git&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> -it k3s-viper kubectl -n vault &lt;span class="nb">exec&lt;/span> -i vault-0 -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> vault kv put secret/platform/aws-budget &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">access_key_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;&amp;lt;paste&amp;gt;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">secret_access_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;&amp;lt;paste&amp;gt;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">region&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;us-east-2&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then let ESO do the sync:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent get externalsecret aws-budget-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You want &lt;code>STATUS: SecretSynced&lt;/code>. Git only ever holds the &lt;code>ExternalSecret&lt;/code> &lt;strong>mapping&lt;/strong> — path names and key names. Never values. And please don&amp;rsquo;t &lt;code>kubectl get secret -o yaml&lt;/code> and paste the result into Slack.&lt;/p>
&lt;h2 id="step-3--build-and-import-the-mcp-image">Step 3 — build and import the MCP image&lt;/h2>
&lt;p>Dockerized k3s cannot see host Docker images, and there is no registry pull for &lt;code>aws-budget-mcp:dev&lt;/code>, so you import it into containerd:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./aws-sandbox-agent/scripts/02-build-import-mcp.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That builds the image and runs &lt;code>ctr images import&lt;/code>. The Deployment uses &lt;code>imagePullPolicy: IfNotPresent&lt;/code> on purpose. &lt;code>ImagePullBackOff&lt;/code> means the import didn&amp;rsquo;t land or your tag doesn&amp;rsquo;t match.&lt;/p>
&lt;h2 id="step-4--apply-the-agent">Step 4 — apply the agent&lt;/h2>
&lt;p>Note the shape of this command. Kustomize runs on the &lt;strong>host&lt;/strong>, and the output is piped into k3s — not &lt;code>apply -k&lt;/code> against a host path the container can&amp;rsquo;t see:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl kustomize aws-sandbox-agent/k8s &lt;span class="p">|&lt;/span> docker &lt;span class="nb">exec&lt;/span> -i k3s-viper kubectl apply -f -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">externalsecret.external-secrets.io/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">deployment.apps/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">service/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">configmap/aws-budget-skills created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/aws-budget-mcp created
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/aws-budget created
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One wrinkle worth knowing: kagent 0.10.0-rc2 &lt;strong>rejects &lt;code>spec.skills&lt;/code>&lt;/strong> on a &lt;code>SandboxAgent&lt;/code>, and there is no skill-mount into the gVisor session. So the skill text lives in ConfigMap &lt;code>aws-budget-skills&lt;/code> and gets inlined into &lt;code>systemMessage&lt;/code> via &lt;code>declarative.promptTemplate&lt;/code>. Keep &lt;code>skills/&lt;/code> and &lt;code>k8s/skills-configmap.yaml&lt;/code> in sync.&lt;/p>
&lt;h2 id="step-5--what-ready-actually-means">Step 5 — what &amp;ldquo;Ready&amp;rdquo; actually means&lt;/h2>
&lt;p>kagent does not start a Deployment of the LLM runtime. It creates an &lt;code>ActorTemplate&lt;/code> owned by the SandboxAgent. Substrate boots a &lt;strong>golden&lt;/strong> actor once, checkpoints it, and stores that checkpoint as &lt;code>status.goldenSnapshot&lt;/code>. New chats restore from that image.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>You see&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>No &lt;code>ActorTemplate&lt;/code>&lt;/td>
&lt;td>Wrong CRD pin (0.0.12) or the controller isn&amp;rsquo;t reconciling&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Template, no &lt;code>goldenSnapshot&lt;/code>&lt;/td>
&lt;td>First checkpoint running (60–90s is normal), or gVisor/atelet can&amp;rsquo;t write storage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>Ready=True&lt;/code> + &lt;code>goldenSnapshot: gs://…&lt;/code>&lt;/td>
&lt;td>You can talk to it&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent get sandboxagents,remotemcpservers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent get actortemplates
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-tool-catalog--no-generic-shell">The tool catalog — no generic shell&lt;/h2>
&lt;p>Eleven tools, all Describe/Get/View/List. This is the single most important design decision in the demo, and it&amp;rsquo;s a design decision, not a config flag:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>AWS API (typical)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>aws_whoami&lt;/code>&lt;/td>
&lt;td>&lt;code>sts:GetCallerIdentity&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_month&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_by_service&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code> grouped by service&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_budgets&lt;/code>&lt;/td>
&lt;td>&lt;code>budgets:ViewBudget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_ec2_capacity&lt;/code>&lt;/td>
&lt;td>&lt;code>ec2:DescribeInstances&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_asg&lt;/code>&lt;/td>
&lt;td>&lt;code>autoscaling:Describe*&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_rds&lt;/code>&lt;/td>
&lt;td>&lt;code>rds:Describe*&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_ebs_summary&lt;/code>&lt;/td>
&lt;td>&lt;code>ec2:DescribeVolumes&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_service_quotas&lt;/code>&lt;/td>
&lt;td>&lt;code>servicequotas:GetServiceQuota&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_rightsizing_hints&lt;/code>&lt;/td>
&lt;td>CE rightsizing / Compute Optimizer&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_executive_brief&lt;/code>&lt;/td>
&lt;td>composes the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is &lt;strong>no&lt;/strong> &amp;ldquo;run any aws cli&amp;rdquo; tool. The agent has no verb for destruction. That boundary is enforced three independent times — in the tool code, in the IAM policy, and in the sandbox — so getting past one layer still lands you on the next.&lt;/p>
&lt;p>Small API detail that trips people up: Cost Explorer and Budgets live in &lt;strong>us-east-1&lt;/strong>. The tools still &lt;em>filter&lt;/em> results to us-east-2.&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>2026-08-16 on Viper, through &lt;code>/api/a2a-sandboxes/kagent/aws-budget&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Month-to-date spend&lt;/td>
&lt;td>&lt;strong>$0.67&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Budget&lt;/td>
&lt;td>&lt;strong>$4.13 / $100&lt;/strong> (4.13% used)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>EC2 / ASG / RDS / EBS&lt;/td>
&lt;td>&lt;strong>0 / 0 / 0 / 0&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Identity&lt;/td>
&lt;td>&lt;code>aws-budget-agent&lt;/code>, account 616973157416&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tool calls&lt;/td>
&lt;td>&lt;strong>10 / 10&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" alt="Live kagent chat: aws-budget reports us-east-2 MTD spend of $0.67, budget $4.13 of $100 used, and zero EC2/ASG/RDS/EBS capacity, with a full status table" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Ten of ten tool calls, every number sourced from a real AWS API.&lt;/em>&lt;/p>
&lt;p>The line I care about most is not the dollar figure. It&amp;rsquo;s this: the agent reported &amp;ldquo;Cost Explorer rightsizing API denied; Compute Optimizer not enrolled&amp;rdquo; instead of inventing a recommendation. An agent that fabricates a helpful-sounding number to fill a gap is an agent you cannot put in front of a budget.&lt;/p>
&lt;p>If yours &lt;em>does&lt;/em> invent spend, the tools didn&amp;rsquo;t run. Tell it &amp;ldquo;call the tools; do not estimate,&amp;rdquo; then check the MCP logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> k3s-viper kubectl -n kagent logs deploy/aws-budget-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That process must never print the secret key.&lt;/p>
&lt;h2 id="proof-with-nothing-sensitive-on-screen">Proof, with nothing sensitive on screen&lt;/h2>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" alt="Terminal showing kubectl output: sandboxagent aws-budget READY True ACCEPTED True, remotemcpserver aws-budget-mcp STREAMABLE_HTTP, pod aws-budget-mcp 1/1 Running" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live capture, 2026-08-16. &lt;code>Ready=True&lt;/code>, MCP &lt;code>Accepted&lt;/code> with 11 tools, pod &lt;code>1/1 Running&lt;/code> — and deliberately no Vault tokens or AWS keys in frame.&lt;/em>&lt;/p>
&lt;p>And the whole turn as a short reel:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" alt="Animated reel of the aws-budget agent handling the spend-and-capacity question end to end" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>One tell that these are genuinely sandboxed sessions: the classic &lt;code>/api/a2a/kagent/aws-budget&lt;/code> endpoint &lt;strong>404s&lt;/strong>, because there is no &lt;code>Agent&lt;/code> CR at all. The UI talks to &lt;code>/api/a2a-sandboxes/kagent/aws-budget&lt;/code>, and the card wears a &lt;em>Sandbox: Agent Substrate&lt;/em> badge.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>The image must be imported on the k3s node (&lt;code>ctr images import&lt;/code>) before the MCP pod starts.&lt;/li>
&lt;li>The Vault path must exist first, or the ExternalSecret sits unsynced forever.&lt;/li>
&lt;li>Nested gVisor on dockerized k3s can still hit &lt;code>runsc&lt;/code> / seccomp / &lt;code>/dev/kvm&lt;/code> problems. That&amp;rsquo;s a worker-environment issue — not a reason to downgrade the agent to an unsandboxed Deployment.&lt;/li>
&lt;li>Snapshots are local rustfs. Cluster-wide object storage is future work.&lt;/li>
&lt;li>Never commit AWS or GCP secret &lt;strong>values&lt;/strong>. Manifests carry paths and key names only.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Full demo — manifests, the FastMCP server, IAM policy, runbooks, and the live report this draws on: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/aws-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / aws-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>A GCP Budget SandboxAgent on kagent — and the Value of an Agent That Says "Unavailable"</title><link>https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/</link><pubDate>Mon, 17 Aug 2026 08:45:00 -0400</pubDate><guid>https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/</guid><description>&lt;p>This is the GCP sibling of my &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget SandboxAgent&lt;/a>: a gVisor-isolated agent that answers&lt;/p>
&lt;blockquote>
&lt;p>What&amp;rsquo;s our us-east1 spend this month, and are we over capacity?&lt;/p>
&lt;/blockquote>
&lt;p>for org &lt;strong>maniak.io&lt;/strong>, with real Cloud Billing and Compute Engine numbers and a service-account JSON that never touches git.&lt;/p>
&lt;p>Here&amp;rsquo;s the thing: on the live run, half of it &lt;strong>didn&amp;rsquo;t work&lt;/strong>. And that turned out to be the most useful part of the demo, so I&amp;rsquo;m publishing it that way rather than re-shooting a clean take.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with five SandboxAgent cards including kagent/gcp-budget" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Isolated sandboxes, not plain Agents — &lt;code>kagent/gcp-budget&lt;/code> is on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation, same as any other pod. Fine for a cluster helper.&lt;/p>
&lt;p>This one reads the GCP bill and the Compute Engine inventory. The model gets a filesystem, memory, and a network for the whole chat. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel is the wall between the model session and my Viper/k3s host. Tools reach GCP through the MCP pod; the service-account JSON stays in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker. The next message restores that same session instead of cold-booting a container.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per executive conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume deterministically.&lt;/li>
&lt;/ul>
&lt;p>If you only needed a Python container with the &lt;code>google-cloud-*&lt;/code> clients and no snapshot lifecycle, a Deployment would do. That&amp;rsquo;s not this.&lt;/p>
&lt;p>&lt;strong>Tradeoff, stated plainly:&lt;/strong> nested gVisor on dockerized k3s is finicky, and snapshots land on in-cluster rustfs today — the &lt;code>gs://&lt;/code> you&amp;rsquo;ll see is a URI prefix, not live GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;gcp-budget-mcp&amp;#34;]
 mcp[&amp;#34;gcp-budget-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 gcp[&amp;#34;GCP us-east1&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/gcp-budget&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; gcp
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The service-account JSON goes Vault → External Secrets Operator → &lt;strong>the MCP pod&lt;/strong>. Not the actor. The session that&amp;rsquo;s running the model and parsing untrusted tool output never holds a Google credential.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Region&lt;/td>
&lt;td>&lt;strong>us-east1&lt;/strong> only&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Org&lt;/td>
&lt;td>&lt;strong>maniak.io&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Projects (names only)&lt;/td>
&lt;td>viper-kagent, maniak-io, qr-maniak-io&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that. &lt;strong>0.0.12&lt;/strong> removed &lt;code>valueFrom&lt;/code> and moved the pause image to &lt;code>SandboxConfig&lt;/code>, so the apiserver rejects rc2&amp;rsquo;s object and you get &lt;code>Ready=False&lt;/code> / &lt;code>ActorTemplateNotFound&lt;/code>.&lt;/p>
&lt;p>Do &lt;strong>not&lt;/strong> &amp;ldquo;upgrade to fix&amp;rdquo; a Ready=False agent. That&amp;rsquo;s how I already burned an afternoon on this lab. A pin mismatch is a CRD problem.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>&lt;strong>1. Service account.&lt;/strong> GCP console, org maniak.io → IAM → create &lt;code>gcp-budget-agent&lt;/code> → attach read-mostly permissions → download the JSON key once, and keep that file outside the repo. Enable Cloud Billing, Cloud Billing Budget, Compute Engine, and Cloud Resource Manager APIs on the projects it will read.&lt;/p>
&lt;p>&lt;strong>2. Vault.&lt;/strong> Path &lt;code>secret/platform/gcp-budget&lt;/code>, keys &lt;code>credentials_json&lt;/code>, &lt;code>billing_account&lt;/code>, &lt;code>project&lt;/code>, &lt;code>region&lt;/code>. ESO syncs it into a Kubernetes Secret consumed only by the MCP pod. Git holds the &lt;code>ExternalSecret&lt;/code> mapping — names, never values.&lt;/p>
&lt;p>&lt;strong>3. Image.&lt;/strong> Build &lt;code>gcp-budget-mcp:dev&lt;/code> on the node and &lt;code>ctr images import&lt;/code> it, because dockerized k3s can&amp;rsquo;t see host Docker images and there&amp;rsquo;s no registry pull for a &lt;code>:dev&lt;/code> tag.&lt;/p>
&lt;p>&lt;strong>4. Apply.&lt;/strong> Kustomize on the host, piped into the cluster:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl kustomize gcp-sandbox-agent/k8s &lt;span class="p">|&lt;/span> docker &lt;span class="nb">exec&lt;/span> -i k3s-viper kubectl apply -f -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>5. Verify.&lt;/strong> Expect this, and nothing sensitive in it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME READY ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/gcp-budget True True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME PROTOCOL URL ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/gcp-budget-mcp STREAMABLE_HTTP http://gcp-budget-mcp.kagent:8084/mcp True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pod/gcp-budget-mcp-5664dfb8f7-pwlwv 1/1 Running 0 4m41s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME CLASS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actortemplate.ate.dev/gcp-budget-82cc62c613737d64 gvisor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">location: gs://ate-snapshots/kagent/gcp-budget
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goldenSnapshot: gs://ate-snapshots/kagent/gcp-budget/5edafe3c-.../2026-08-16T17:55:34Z-XIKENZRG7IHYGCONXJZFBBFY2R
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">phase: Ready
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Ready=True&lt;/code> plus a &lt;code>goldenSnapshot&lt;/code> means Substrate booted a golden actor, checkpointed it, and new chats will restore from that image.&lt;/p>
&lt;h2 id="the-tools">The tools&lt;/h2>
&lt;p>Eight, all read-only:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>What it reads&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gcp_whoami&lt;/code>&lt;/td>
&lt;td>The service-account identity actually in use&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_cost_month&lt;/code>&lt;/td>
&lt;td>Month-to-date attempt via Cloud Billing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_cost_by_service&lt;/code>&lt;/td>
&lt;td>Per-service breakdown&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_budgets&lt;/code>&lt;/td>
&lt;td>Cloud Billing Budgets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_projects&lt;/code>&lt;/td>
&lt;td>Resource Manager project list&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_compute_capacity&lt;/code>&lt;/td>
&lt;td>Instances and disks, zone-filtered to us-east1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_quotas&lt;/code>&lt;/td>
&lt;td>Compute quotas vs. usage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_executive_brief&lt;/code>&lt;/td>
&lt;td>Composes the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>No generic &amp;ldquo;run any gcloud&amp;rdquo; tool. No project-delete, no IAM-create.&lt;/p>
&lt;h2 id="the-live-run-including-what-broke">The live run, including what broke&lt;/h2>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-chat-session.png" alt="Live gcp-budget chat: billing account status unavailable due to a tool/runtime import error, MTD spend unavailable from Cloud Billing APIs, with the ImportError quoted and a Resource Manager project list" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Five tool calls on Q1, three on Q2. Billing unavailable with the error quoted verbatim; capacity answered cleanly.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What&amp;rsquo;s our GCP budget status and which projects are on the billing account?&amp;rdquo;&lt;/strong> (~18s, tools: &lt;code>gcp_whoami&lt;/code>, &lt;code>gcp_cost_month&lt;/code>, &lt;code>gcp_budgets&lt;/code>, &lt;code>gcp_cost_by_service&lt;/code>, &lt;code>gcp_projects&lt;/code>)&lt;/p>
&lt;p>Billing account &lt;code>011C38-867461-BE95B1&lt;/code>, linked projects, budgets, and MTD spend all came back &lt;strong>unavailable&lt;/strong>. The runtime error, quoted in the answer:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ImportError: cannot import name &amp;#39;billing_budgets_v1&amp;#39; from &amp;#39;google.cloud&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The agent then did something I want to highlight. It separately checked Resource Manager, listed &lt;code>viper-kagent&lt;/code>, &lt;code>maniak-io&lt;/code>, &lt;code>qr-maniak-io&lt;/code> — and explicitly noted that &lt;strong>Resource Manager visibility is not the same thing as billing-account linkage.&lt;/strong> It didn&amp;rsquo;t quietly present one as the other. My known lab budget (&lt;code>trail budget&lt;/code>, $1) was not returned, and it said so.&lt;/p>
&lt;p>&lt;strong>After the rebuild.&lt;/strong> The import was wrong: &lt;code>google-cloud-billing-budgets==1.21.0&lt;/code> was pinned and installed, but the old alias &lt;code>from google.cloud import billing_budgets_v1&lt;/code> doesn&amp;rsquo;t exist. The working path is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">google.cloud.billing&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">budgets_v1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">budgets_v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BudgetServiceClient&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Rebuilt as &lt;code>gcp-budget-mcp:dev&lt;/code>, new pod, re-ran Q1. No more &lt;code>ImportError&lt;/code> — and Cloud Billing then returned &lt;code>Unauthenticated&lt;/code> for &lt;code>billing.accounts.get&lt;/code>, &lt;code>billingbudgets.budgets.list&lt;/code>, and cost-by-service. Resource Manager still listed the three projects. The $1 trail budget still didn&amp;rsquo;t appear. Month-to-date spend still unavailable.&lt;/p>
&lt;p>Two failures in a row, and at no point did a dollar amount get invented.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;Any compute running in us-east1, and are we near quota?&amp;rdquo;&lt;/strong> (~15s, tools: &lt;code>gcp_projects&lt;/code>, &lt;code>gcp_compute_capacity&lt;/code>, &lt;code>gcp_quotas&lt;/code>)&lt;/p>
&lt;p>This half worked perfectly:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Quota&lt;/th>
&lt;th style="text-align:right">Usage&lt;/th>
&lt;th style="text-align:right">Limit&lt;/th>
&lt;th>Status&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CPUs&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">200&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Instances&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">24&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Total disk GB&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">4096&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SSD total GB&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">500&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>In-use addresses&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Zero instances running, zero stopped, zero disks. Not near quota.&lt;/p>
&lt;h2 id="the-cloud-billing-gap-is-real-not-a-bug-in-my-code">The Cloud Billing gap is real, not a bug in my code&lt;/h2>
&lt;p>Worth separating two different problems here.&lt;/p>
&lt;p>The &lt;code>ImportError&lt;/code> was my bug and I fixed it. The &lt;code>Unauthenticated&lt;/code> is a permissions gap on that service account. But underneath both sits a structural fact: &lt;strong>Cloud Billing Accounts, Budgets, and Catalog do not expose month-to-date spend.&lt;/strong> There is no &amp;ldquo;what have I spent so far this month&amp;rdquo; call in that surface the way &lt;code>ce:GetCostAndUsage&lt;/code> gives you on AWS. Real MTD on GCP means BigQuery billing export.&lt;/p>
&lt;p>So the tools are written to say &lt;strong>unavailable&lt;/strong> rather than approximate. That&amp;rsquo;s the design position, and it&amp;rsquo;s why the AWS agent can report &lt;code>$0.67&lt;/code> while this one can&amp;rsquo;t report anything.&lt;/p>
&lt;h2 id="why-unavailable-is-the-feature">Why &amp;ldquo;unavailable&amp;rdquo; is the feature&lt;/h2>
&lt;p>It would have been easy to make this demo look better. Return &lt;code>$0.00&lt;/code> on a failed billing call. Present the Resource Manager project list as &amp;ldquo;projects on the billing account.&amp;rdquo; Round something plausible. Every one of those makes a nicer screenshot and a worse agent.&lt;/p>
&lt;p>An agent wired to your finances has exactly one job when a tool fails: &lt;strong>say the tool failed.&lt;/strong> A confident wrong number is worse than a blank, because a blank gets escalated and a wrong number gets acted on. The whole point of putting keys in Vault, tools behind MCP, and the session behind gVisor is to earn trust in what the agent reports — and you throw that away the first time it papers over a gap to be helpful.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the image on the k3s node before the MCP pod starts.&lt;/li>
&lt;li>The Vault path must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>Cloud Billing does not return MTD spend. The tools say unavailable.&lt;/li>
&lt;li>Billing APIs returned &lt;code>Unauthenticated&lt;/code> on the live run. That&amp;rsquo;s a permissions gap I haven&amp;rsquo;t closed, not a fixed result.&lt;/li>
&lt;li>No generic gcloud tool, no project-delete, no IAM-create.&lt;/li>
&lt;li>Snapshots are in-cluster rustfs. &lt;code>gs://&lt;/code> is a prefix only.&lt;/li>
&lt;li>Never commit the service-account JSON or Vault secret &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Full demo — manifests, the FastMCP server, security notes, runbooks, and the live report: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/gcp-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / gcp-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>This is the GCP sibling of my &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget SandboxAgent&lt;/a>: a gVisor-isolated agent that answers&lt;/p>
&lt;blockquote>
&lt;p>What&amp;rsquo;s our us-east1 spend this month, and are we over capacity?&lt;/p>
&lt;/blockquote>
&lt;p>for org &lt;strong>maniak.io&lt;/strong>, with real Cloud Billing and Compute Engine numbers and a service-account JSON that never touches git.&lt;/p>
&lt;p>Here&amp;rsquo;s the thing: on the live run, half of it &lt;strong>didn&amp;rsquo;t work&lt;/strong>. And that turned out to be the most useful part of the demo, so I&amp;rsquo;m publishing it that way rather than re-shooting a clean take.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with five SandboxAgent cards including kagent/gcp-budget" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Isolated sandboxes, not plain Agents — &lt;code>kagent/gcp-budget&lt;/code> is on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation, same as any other pod. Fine for a cluster helper.&lt;/p>
&lt;p>This one reads the GCP bill and the Compute Engine inventory. The model gets a filesystem, memory, and a network for the whole chat. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel is the wall between the model session and my Viper/k3s host. Tools reach GCP through the MCP pod; the service-account JSON stays in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker. The next message restores that same session instead of cold-booting a container.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per executive conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume deterministically.&lt;/li>
&lt;/ul>
&lt;p>If you only needed a Python container with the &lt;code>google-cloud-*&lt;/code> clients and no snapshot lifecycle, a Deployment would do. That&amp;rsquo;s not this.&lt;/p>
&lt;p>&lt;strong>Tradeoff, stated plainly:&lt;/strong> nested gVisor on dockerized k3s is finicky, and snapshots land on in-cluster rustfs today — the &lt;code>gs://&lt;/code> you&amp;rsquo;ll see is a URI prefix, not live GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;gcp-budget-mcp&amp;#34;]
 mcp[&amp;#34;gcp-budget-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 gcp[&amp;#34;GCP us-east1&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/gcp-budget&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; gcp
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The service-account JSON goes Vault → External Secrets Operator → &lt;strong>the MCP pod&lt;/strong>. Not the actor. The session that&amp;rsquo;s running the model and parsing untrusted tool output never holds a Google credential.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Region&lt;/td>
&lt;td>&lt;strong>us-east1&lt;/strong> only&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Org&lt;/td>
&lt;td>&lt;strong>maniak.io&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Projects (names only)&lt;/td>
&lt;td>viper-kagent, maniak-io, qr-maniak-io&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that. &lt;strong>0.0.12&lt;/strong> removed &lt;code>valueFrom&lt;/code> and moved the pause image to &lt;code>SandboxConfig&lt;/code>, so the apiserver rejects rc2&amp;rsquo;s object and you get &lt;code>Ready=False&lt;/code> / &lt;code>ActorTemplateNotFound&lt;/code>.&lt;/p>
&lt;p>Do &lt;strong>not&lt;/strong> &amp;ldquo;upgrade to fix&amp;rdquo; a Ready=False agent. That&amp;rsquo;s how I already burned an afternoon on this lab. A pin mismatch is a CRD problem.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>&lt;strong>1. Service account.&lt;/strong> GCP console, org maniak.io → IAM → create &lt;code>gcp-budget-agent&lt;/code> → attach read-mostly permissions → download the JSON key once, and keep that file outside the repo. Enable Cloud Billing, Cloud Billing Budget, Compute Engine, and Cloud Resource Manager APIs on the projects it will read.&lt;/p>
&lt;p>&lt;strong>2. Vault.&lt;/strong> Path &lt;code>secret/platform/gcp-budget&lt;/code>, keys &lt;code>credentials_json&lt;/code>, &lt;code>billing_account&lt;/code>, &lt;code>project&lt;/code>, &lt;code>region&lt;/code>. ESO syncs it into a Kubernetes Secret consumed only by the MCP pod. Git holds the &lt;code>ExternalSecret&lt;/code> mapping — names, never values.&lt;/p>
&lt;p>&lt;strong>3. Image.&lt;/strong> Build &lt;code>gcp-budget-mcp:dev&lt;/code> on the node and &lt;code>ctr images import&lt;/code> it, because dockerized k3s can&amp;rsquo;t see host Docker images and there&amp;rsquo;s no registry pull for a &lt;code>:dev&lt;/code> tag.&lt;/p>
&lt;p>&lt;strong>4. Apply.&lt;/strong> Kustomize on the host, piped into the cluster:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl kustomize gcp-sandbox-agent/k8s &lt;span class="p">|&lt;/span> docker &lt;span class="nb">exec&lt;/span> -i k3s-viper kubectl apply -f -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>5. Verify.&lt;/strong> Expect this, and nothing sensitive in it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME READY ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/gcp-budget True True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME PROTOCOL URL ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/gcp-budget-mcp STREAMABLE_HTTP http://gcp-budget-mcp.kagent:8084/mcp True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pod/gcp-budget-mcp-5664dfb8f7-pwlwv 1/1 Running 0 4m41s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME CLASS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actortemplate.ate.dev/gcp-budget-82cc62c613737d64 gvisor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">location: gs://ate-snapshots/kagent/gcp-budget
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goldenSnapshot: gs://ate-snapshots/kagent/gcp-budget/5edafe3c-.../2026-08-16T17:55:34Z-XIKENZRG7IHYGCONXJZFBBFY2R
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">phase: Ready
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Ready=True&lt;/code> plus a &lt;code>goldenSnapshot&lt;/code> means Substrate booted a golden actor, checkpointed it, and new chats will restore from that image.&lt;/p>
&lt;h2 id="the-tools">The tools&lt;/h2>
&lt;p>Eight, all read-only:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>What it reads&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gcp_whoami&lt;/code>&lt;/td>
&lt;td>The service-account identity actually in use&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_cost_month&lt;/code>&lt;/td>
&lt;td>Month-to-date attempt via Cloud Billing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_cost_by_service&lt;/code>&lt;/td>
&lt;td>Per-service breakdown&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_budgets&lt;/code>&lt;/td>
&lt;td>Cloud Billing Budgets&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_projects&lt;/code>&lt;/td>
&lt;td>Resource Manager project list&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_compute_capacity&lt;/code>&lt;/td>
&lt;td>Instances and disks, zone-filtered to us-east1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_quotas&lt;/code>&lt;/td>
&lt;td>Compute quotas vs. usage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gcp_executive_brief&lt;/code>&lt;/td>
&lt;td>Composes the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>No generic &amp;ldquo;run any gcloud&amp;rdquo; tool. No project-delete, no IAM-create.&lt;/p>
&lt;h2 id="the-live-run-including-what-broke">The live run, including what broke&lt;/h2>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-gcp-budget-sandboxagent/ui-chat-session.png" alt="Live gcp-budget chat: billing account status unavailable due to a tool/runtime import error, MTD spend unavailable from Cloud Billing APIs, with the ImportError quoted and a Resource Manager project list" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Five tool calls on Q1, three on Q2. Billing unavailable with the error quoted verbatim; capacity answered cleanly.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What&amp;rsquo;s our GCP budget status and which projects are on the billing account?&amp;rdquo;&lt;/strong> (~18s, tools: &lt;code>gcp_whoami&lt;/code>, &lt;code>gcp_cost_month&lt;/code>, &lt;code>gcp_budgets&lt;/code>, &lt;code>gcp_cost_by_service&lt;/code>, &lt;code>gcp_projects&lt;/code>)&lt;/p>
&lt;p>Billing account &lt;code>011C38-867461-BE95B1&lt;/code>, linked projects, budgets, and MTD spend all came back &lt;strong>unavailable&lt;/strong>. The runtime error, quoted in the answer:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">ImportError: cannot import name &amp;#39;billing_budgets_v1&amp;#39; from &amp;#39;google.cloud&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The agent then did something I want to highlight. It separately checked Resource Manager, listed &lt;code>viper-kagent&lt;/code>, &lt;code>maniak-io&lt;/code>, &lt;code>qr-maniak-io&lt;/code> — and explicitly noted that &lt;strong>Resource Manager visibility is not the same thing as billing-account linkage.&lt;/strong> It didn&amp;rsquo;t quietly present one as the other. My known lab budget (&lt;code>trail budget&lt;/code>, $1) was not returned, and it said so.&lt;/p>
&lt;p>&lt;strong>After the rebuild.&lt;/strong> The import was wrong: &lt;code>google-cloud-billing-budgets==1.21.0&lt;/code> was pinned and installed, but the old alias &lt;code>from google.cloud import billing_budgets_v1&lt;/code> doesn&amp;rsquo;t exist. The working path is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">google.cloud.billing&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">budgets_v1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">budgets_v1&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">BudgetServiceClient&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Rebuilt as &lt;code>gcp-budget-mcp:dev&lt;/code>, new pod, re-ran Q1. No more &lt;code>ImportError&lt;/code> — and Cloud Billing then returned &lt;code>Unauthenticated&lt;/code> for &lt;code>billing.accounts.get&lt;/code>, &lt;code>billingbudgets.budgets.list&lt;/code>, and cost-by-service. Resource Manager still listed the three projects. The $1 trail budget still didn&amp;rsquo;t appear. Month-to-date spend still unavailable.&lt;/p>
&lt;p>Two failures in a row, and at no point did a dollar amount get invented.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;Any compute running in us-east1, and are we near quota?&amp;rdquo;&lt;/strong> (~15s, tools: &lt;code>gcp_projects&lt;/code>, &lt;code>gcp_compute_capacity&lt;/code>, &lt;code>gcp_quotas&lt;/code>)&lt;/p>
&lt;p>This half worked perfectly:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Quota&lt;/th>
&lt;th style="text-align:right">Usage&lt;/th>
&lt;th style="text-align:right">Limit&lt;/th>
&lt;th>Status&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CPUs&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">200&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Instances&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">24&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Total disk GB&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">4096&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SSD total GB&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">500&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>In-use addresses&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td>OK&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Zero instances running, zero stopped, zero disks. Not near quota.&lt;/p>
&lt;h2 id="the-cloud-billing-gap-is-real-not-a-bug-in-my-code">The Cloud Billing gap is real, not a bug in my code&lt;/h2>
&lt;p>Worth separating two different problems here.&lt;/p>
&lt;p>The &lt;code>ImportError&lt;/code> was my bug and I fixed it. The &lt;code>Unauthenticated&lt;/code> is a permissions gap on that service account. But underneath both sits a structural fact: &lt;strong>Cloud Billing Accounts, Budgets, and Catalog do not expose month-to-date spend.&lt;/strong> There is no &amp;ldquo;what have I spent so far this month&amp;rdquo; call in that surface the way &lt;code>ce:GetCostAndUsage&lt;/code> gives you on AWS. Real MTD on GCP means BigQuery billing export.&lt;/p>
&lt;p>So the tools are written to say &lt;strong>unavailable&lt;/strong> rather than approximate. That&amp;rsquo;s the design position, and it&amp;rsquo;s why the AWS agent can report &lt;code>$0.67&lt;/code> while this one can&amp;rsquo;t report anything.&lt;/p>
&lt;h2 id="why-unavailable-is-the-feature">Why &amp;ldquo;unavailable&amp;rdquo; is the feature&lt;/h2>
&lt;p>It would have been easy to make this demo look better. Return &lt;code>$0.00&lt;/code> on a failed billing call. Present the Resource Manager project list as &amp;ldquo;projects on the billing account.&amp;rdquo; Round something plausible. Every one of those makes a nicer screenshot and a worse agent.&lt;/p>
&lt;p>An agent wired to your finances has exactly one job when a tool fails: &lt;strong>say the tool failed.&lt;/strong> A confident wrong number is worse than a blank, because a blank gets escalated and a wrong number gets acted on. The whole point of putting keys in Vault, tools behind MCP, and the session behind gVisor is to earn trust in what the agent reports — and you throw that away the first time it papers over a gap to be helpful.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the image on the k3s node before the MCP pod starts.&lt;/li>
&lt;li>The Vault path must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>Cloud Billing does not return MTD spend. The tools say unavailable.&lt;/li>
&lt;li>Billing APIs returned &lt;code>Unauthenticated&lt;/code> on the live run. That&amp;rsquo;s a permissions gap I haven&amp;rsquo;t closed, not a fixed result.&lt;/li>
&lt;li>No generic gcloud tool, no project-delete, no IAM-create.&lt;/li>
&lt;li>Snapshots are in-cluster rustfs. &lt;code>gs://&lt;/code> is a prefix only.&lt;/li>
&lt;li>Never commit the service-account JSON or Vault secret &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Full demo — manifests, the FastMCP server, security notes, runbooks, and the live report: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/gcp-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / gcp-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>A ServiceNow Triage SandboxAgent on kagent — Where Write Tools Belong</title><link>https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/</link><pubDate>Mon, 17 Aug 2026 08:30:00 -0400</pubDate><guid>https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/</guid><description>&lt;p>Every ITSM demo I&amp;rsquo;ve ever sat through answers &amp;ldquo;how many tickets are open.&amp;rdquo; That&amp;rsquo;s a &lt;code>COUNT(*)&lt;/code>. The actual manager question is harder:&lt;/p>
&lt;blockquote>
&lt;p>What IT tickets are open right now, and how should we organize them?&lt;/p>
&lt;/blockquote>
&lt;p>That needs priority buckets, someone to notice the unassigned P1, and a list you can act on in a standup. So I built it as a gVisor &lt;code>SandboxAgent&lt;/code> on kagent + Agent Substrate, pointed at a real ServiceNow personal developer instance, and — unlike my &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP&lt;/a> budget agents — I gave this one &lt;strong>write tools&lt;/strong>. Two of them. That changes the design conversation.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with SandboxAgent cards — aws-budget, fortigate, hello-substrate, servicenow" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Isolated sandboxes, not plain Agents — &lt;code>kagent/servicenow&lt;/code> on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation. Fine for a cluster helper.&lt;/p>
&lt;p>This one talks to a ticketing system full of text other people wrote. That matters more than it sounds — incident descriptions, work notes, and short descriptions are &lt;strong>untrusted input&lt;/strong> flowing straight into a model that holds tools. Prompt injection in a ticket body is not a hypothetical attack; it&amp;rsquo;s the obvious one.&lt;/p>
&lt;p>Substrate puts the session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel sits between the model session and my Viper/k3s host. Tools call ServiceNow through the MCP pod; username and password stay in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker; the next message restores the same session.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per manager conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Tradeoff on this lab:&lt;/strong> nested gVisor on dockerized k3s, and snapshots are in-cluster rustfs today (&lt;code>gs://&lt;/code> is a URI prefix only), not GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Manager chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;servicenow-mcp&amp;#34;]
 mcp[&amp;#34;servicenow-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 sn[&amp;#34;ServiceNow&amp;lt;br/&amp;gt;Table API&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/servicenow&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; sn
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The ServiceNow password goes Vault → ESO → &lt;strong>the MCP pod&lt;/strong>. The actor calls tools; it never sees a credential.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Host (name only)&lt;/td>
&lt;td>&lt;code>https://dev203166.service-now.com&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 always writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that shape; &lt;strong>0.0.12&lt;/strong> does not — it dropped &lt;code>valueFrom&lt;/code> and moved the pause image into &lt;code>SandboxConfig&lt;/code>, so the apiserver rejects rc2&amp;rsquo;s object. Don&amp;rsquo;t &amp;ldquo;upgrade to fix&amp;rdquo; a &lt;code>Ready=False&lt;/code> agent.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>&lt;strong>1. Vault.&lt;/strong> Path &lt;code>secret/platform/servicenow&lt;/code>, keys &lt;code>host&lt;/code>, &lt;code>username&lt;/code>, &lt;code>password&lt;/code>. Only the &lt;strong>host name&lt;/strong> is safe to commit. ESO syncs into a Kubernetes Secret the MCP pod consumes.&lt;/p>
&lt;p>&lt;strong>2. Image.&lt;/strong> Build &lt;code>servicenow-mcp:dev&lt;/code> and &lt;code>ctr images import&lt;/code> it onto the k3s node — dockerized k3s can&amp;rsquo;t see host Docker images, and &lt;code>imagePullPolicy: IfNotPresent&lt;/code> is deliberate.&lt;/p>
&lt;p>&lt;strong>3. Apply.&lt;/strong> Kustomize on the host, piped in:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl kustomize service-now-sandbox-agent/k8s &lt;span class="p">|&lt;/span> docker &lt;span class="nb">exec&lt;/span> -i k3s-viper kubectl apply -f -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You get the skills ConfigMap, the MCP Service + Deployment, the ExternalSecret, the RemoteMCPServer, and the SandboxAgent.&lt;/p>
&lt;p>&lt;strong>4. Verify.&lt;/strong> What I had live on 2026-08-16:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Object&lt;/th>
&lt;th>Status&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>SandboxAgent &lt;code>servicenow&lt;/code>&lt;/td>
&lt;td>&lt;code>Ready=True&lt;/code>, &lt;code>Accepted=True&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RemoteMCPServer &lt;code>servicenow-mcp&lt;/code>&lt;/td>
&lt;td>Accepted, &lt;strong>8&lt;/strong> tools, &lt;code>STREAMABLE_HTTP&lt;/code> &lt;code>http://servicenow-mcp.kagent:8084/mcp&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MCP pod&lt;/td>
&lt;td>&lt;code>servicenow-mcp-7c6c455c65-kvnrq&lt;/code> &lt;code>1/1&lt;/code> Running, 0 restarts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ActorTemplate&lt;/td>
&lt;td>&lt;code>servicenow-f5f2dec1f2a81a41&lt;/code>, gvisor, Ready&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Snapshot prefix&lt;/td>
&lt;td>&lt;code>gs://ate-snapshots/kagent/servicenow&lt;/code> (rustfs)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Golden snapshot&lt;/td>
&lt;td>&lt;code>2026-08-16T15:45:24Z-HYSFX5R3DHWLRWYVP7YXOAF4SN&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/cli-live-status.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/cli-live-status.png" alt="Terminal showing SandboxAgent Ready, RemoteMCPServer Accepted with 8 tools, and the golden snapshot on rustfs" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live CLI, 2026-08-16. Nothing sensitive in frame.&lt;/em>&lt;/p>
&lt;h2 id="six-read-tools-two-write-tools">Six read tools, two write tools&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Kind&lt;/th>
&lt;th>What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>sn_whoami&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Which ServiceNow user the agent is acting as&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_list_incidents&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Active incidents, compact rows&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_get_incident&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>One incident by number or sys_id&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_search_incidents&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Encoded-query search&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_incident_summary&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Aggregated counts by field&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_list_requested_items&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Service catalog requested items&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_add_work_note&lt;/code>&lt;/td>
&lt;td>&lt;strong>write&lt;/strong>&lt;/td>
&lt;td>Appends a work note to an incident&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_assign_incident&lt;/code>&lt;/td>
&lt;td>&lt;strong>write&lt;/strong>&lt;/td>
&lt;td>Sets an assignee&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is &lt;strong>no generic shell&lt;/strong> and no HTTP passthrough — you cannot ask this agent to hit an arbitrary Table API path. And critically: &lt;strong>no create, no close, no delete.&lt;/strong> An incident cannot be conjured or disappeared.&lt;/p>
&lt;h2 id="where-write-tools-belong">Where write tools belong&lt;/h2>
&lt;p>This is the part worth arguing about, so let me be direct about the position.&lt;/p>
&lt;p>Read-only agents are easy to trust and frequently useless. &amp;ldquo;Here are your 25 open tickets&amp;rdquo; is genuinely helpful, but the manager&amp;rsquo;s next sentence is always &lt;em>&amp;ldquo;okay, assign the payroll one to someone and note that we&amp;rsquo;re on it.&amp;rdquo;&lt;/em> If the agent can&amp;rsquo;t do that, you alt-tab into ServiceNow and the agent was a fancy report.&lt;/p>
&lt;p>So the two writes exist. What makes them defensible is not that they&amp;rsquo;re small — it&amp;rsquo;s that they&amp;rsquo;re &lt;strong>specific and reversible&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>sn_add_work_note&lt;/code> is &lt;strong>append-only&lt;/strong>. It cannot overwrite history. Worst case, someone gets a spurious note on a ticket, and the note is signed by the agent&amp;rsquo;s identity.&lt;/li>
&lt;li>&lt;code>sn_assign_incident&lt;/code> sets one field. Wrong assignee is an annoyance, fixed by setting it again.&lt;/li>
&lt;/ul>
&lt;p>Compare that to what I deliberately did &lt;strong>not&lt;/strong> build. A &lt;code>sn_close_incident&lt;/code> destroys the audit story — a closed ticket stops getting looked at, which is exactly the outcome a prompt injection would want. A generic &lt;code>sn_table_patch&lt;/code> is a shell with extra steps: any field, any table, including &lt;code>sys_user&lt;/code> roles.&lt;/p>
&lt;p>On top of that, the agent&amp;rsquo;s instructions require it to &lt;strong>ask before writing.&lt;/strong> That&amp;rsquo;s a soft control — a model can be talked out of a soft control, which is precisely why it&amp;rsquo;s the &lt;em>third&lt;/em> layer and not the only one. The hard controls are the tool catalog (there is no verb for &amp;ldquo;close&amp;rdquo;) and the ServiceNow account&amp;rsquo;s own role grants. The prompt-level &amp;ldquo;ask first&amp;rdquo; is there for the ordinary case where the manager and the agent are just working through a list, not for the adversarial one.&lt;/p>
&lt;p>The general rule I&amp;rsquo;d apply to any agent touching a system of record: &lt;strong>give it writes that a human can undo in one click, and nothing else.&lt;/strong>&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>Two questions through &lt;code>/api/a2a-sandboxes/kagent/servicenow&lt;/code>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-chat-session.png" alt="Live servicenow chat: 25 active incidents broken into P1 Critical 13, P2 High 4, P3 Moderate 5, P5 Planning 3, with full incident tables and a needs-eyes unassigned section" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three tool calls: &lt;code>sn_whoami&lt;/code>, &lt;code>sn_incident_summary&lt;/code>, &lt;code>sn_list_incidents&lt;/code>.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What IT tickets are open right now? Give me a manager summary by priority.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>Acting as &lt;strong>System Administrator (admin)&lt;/strong> on &lt;code>dev203166.service-now.com&lt;/code>. 25 active incidents on the current page:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Priority&lt;/th>
&lt;th style="text-align:right">Count&lt;/th>
&lt;th>Manager view&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>P1 – Critical&lt;/td>
&lt;td style="text-align:right">13&lt;/td>
&lt;td>Payroll server, DNS, wireless/network, VPN, Exchange, SAP apps&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P2 – High&lt;/td>
&lt;td style="text-align:right">4&lt;/td>
&lt;td>Network storage, SAP login, hotfix removal, VM launch&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P3 – Moderate&lt;/td>
&lt;td style="text-align:right">5&lt;/td>
&lt;td>Desk phone, email, service request, website JavaScript, SFA access&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P5 – Planning&lt;/td>
&lt;td style="text-align:right">3&lt;/td>
&lt;td>Weather report, replacement iPhone, Oregon mail server&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Then the section that makes it a briefing rather than a dump — &lt;strong>needs eyes / unassigned&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>INC0007001&lt;/strong> — P1 Critical, New, unassigned: &lt;em>employee payroll application server is down&lt;/em>&lt;/li>
&lt;li>&lt;strong>INC0000046&lt;/strong> — P3 Moderate, New, unassigned: &lt;em>can&amp;rsquo;t access SFA software&lt;/em>&lt;/li>
&lt;li>&lt;strong>INC0000039&lt;/strong> — P5 Planning, New, unassigned: &lt;em>trouble getting to Oregon mail server&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>An unassigned P1 on a payroll server is exactly the thing that falls through a queue view, and it&amp;rsquo;s the first thing the agent surfaced.&lt;/p>
&lt;p>&lt;strong>The honest bit:&lt;/strong> &lt;code>sn_incident_summary&lt;/code> — the aggregate-count tool — &lt;strong>failed&lt;/strong> on this run. The agent said so in the answer (&amp;ldquo;the formal summary count tool failed, so priority totals below are from the returned incident list only&amp;rdquo;) and derived the buckets from the rows it actually had. It didn&amp;rsquo;t present a computed total as an API total. Same principle as the &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP agent saying &lt;em>unavailable&lt;/em>&lt;/a>: tell me where the number came from.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;What about the critical VPN and DNS ones?&amp;rdquo;&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Incident&lt;/th>
&lt;th>Short description&lt;/th>
&lt;th>State&lt;/th>
&lt;th>Assignee&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>INC0000015&lt;/td>
&lt;td>VPN client won&amp;rsquo;t launch since the last software update&lt;/td>
&lt;td>In Progress&lt;/td>
&lt;td>Don Goodliffe&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INC0000016&lt;/td>
&lt;td>Rain is leaking on the main DNS server&lt;/td>
&lt;td>In Progress&lt;/td>
&lt;td>ITIL User&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Rain leaking on the DNS server is stock ServiceNow demo data, and I love it too much to swap for something plausible.&lt;/p>
&lt;p>Here&amp;rsquo;s the same session as a scroll:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/kagent-servicenow-live.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/kagent-servicenow-live.gif" alt="Animated scroll of the live servicenow chat session" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>One tell that this is a real sandboxed session: &lt;code>/api/a2a/kagent/servicenow&lt;/code> &lt;strong>404s&lt;/strong> — there is no &lt;code>Agent&lt;/code> CR — and the UI posts to &lt;code>/api/a2a-sandboxes/kagent/servicenow&lt;/code>. The card wears a &lt;em>Sandbox: Agent Substrate&lt;/em> badge.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the MCP image on the k3s node before the pod starts.&lt;/li>
&lt;li>The Vault path must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>No generic shell, no HTTP passthrough, no incident create / close / delete.&lt;/li>
&lt;li>The two write tools exist and the agent must ask first — that&amp;rsquo;s a prompt-level control backed by a tool-level one, not a substitute for it.&lt;/li>
&lt;li>&lt;code>sn_incident_summary&lt;/code> failed on the live run. Priority totals came from the returned rows.&lt;/li>
&lt;li>This is a personal developer instance with stock demo data, not production volume.&lt;/li>
&lt;li>Never commit the ServiceNow username or password &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Full demo — manifests, the FastMCP server, security notes, runbooks, and the live report: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/service-now-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / service-now-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>Every ITSM demo I&amp;rsquo;ve ever sat through answers &amp;ldquo;how many tickets are open.&amp;rdquo; That&amp;rsquo;s a &lt;code>COUNT(*)&lt;/code>. The actual manager question is harder:&lt;/p>
&lt;blockquote>
&lt;p>What IT tickets are open right now, and how should we organize them?&lt;/p>
&lt;/blockquote>
&lt;p>That needs priority buckets, someone to notice the unassigned P1, and a list you can act on in a standup. So I built it as a gVisor &lt;code>SandboxAgent&lt;/code> on kagent + Agent Substrate, pointed at a real ServiceNow personal developer instance, and — unlike my &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP&lt;/a> budget agents — I gave this one &lt;strong>write tools&lt;/strong>. Two of them. That changes the design conversation.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with SandboxAgent cards — aws-budget, fortigate, hello-substrate, servicenow" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Isolated sandboxes, not plain Agents — &lt;code>kagent/servicenow&lt;/code> on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation. Fine for a cluster helper.&lt;/p>
&lt;p>This one talks to a ticketing system full of text other people wrote. That matters more than it sounds — incident descriptions, work notes, and short descriptions are &lt;strong>untrusted input&lt;/strong> flowing straight into a model that holds tools. Prompt injection in a ticket body is not a hypothetical attack; it&amp;rsquo;s the obvious one.&lt;/p>
&lt;p>Substrate puts the session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel sits between the model session and my Viper/k3s host. Tools call ServiceNow through the MCP pod; username and password stay in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker; the next message restores the same session.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per manager conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Tradeoff on this lab:&lt;/strong> nested gVisor on dockerized k3s, and snapshots are in-cluster rustfs today (&lt;code>gs://&lt;/code> is a URI prefix only), not GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Manager chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;servicenow-mcp&amp;#34;]
 mcp[&amp;#34;servicenow-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 sn[&amp;#34;ServiceNow&amp;lt;br/&amp;gt;Table API&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/servicenow&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; sn
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The ServiceNow password goes Vault → ESO → &lt;strong>the MCP pod&lt;/strong>. The actor calls tools; it never sees a credential.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Host (name only)&lt;/td>
&lt;td>&lt;code>https://dev203166.service-now.com&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 always writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that shape; &lt;strong>0.0.12&lt;/strong> does not — it dropped &lt;code>valueFrom&lt;/code> and moved the pause image into &lt;code>SandboxConfig&lt;/code>, so the apiserver rejects rc2&amp;rsquo;s object. Don&amp;rsquo;t &amp;ldquo;upgrade to fix&amp;rdquo; a &lt;code>Ready=False&lt;/code> agent.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>&lt;strong>1. Vault.&lt;/strong> Path &lt;code>secret/platform/servicenow&lt;/code>, keys &lt;code>host&lt;/code>, &lt;code>username&lt;/code>, &lt;code>password&lt;/code>. Only the &lt;strong>host name&lt;/strong> is safe to commit. ESO syncs into a Kubernetes Secret the MCP pod consumes.&lt;/p>
&lt;p>&lt;strong>2. Image.&lt;/strong> Build &lt;code>servicenow-mcp:dev&lt;/code> and &lt;code>ctr images import&lt;/code> it onto the k3s node — dockerized k3s can&amp;rsquo;t see host Docker images, and &lt;code>imagePullPolicy: IfNotPresent&lt;/code> is deliberate.&lt;/p>
&lt;p>&lt;strong>3. Apply.&lt;/strong> Kustomize on the host, piped in:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl kustomize service-now-sandbox-agent/k8s &lt;span class="p">|&lt;/span> docker &lt;span class="nb">exec&lt;/span> -i k3s-viper kubectl apply -f -
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You get the skills ConfigMap, the MCP Service + Deployment, the ExternalSecret, the RemoteMCPServer, and the SandboxAgent.&lt;/p>
&lt;p>&lt;strong>4. Verify.&lt;/strong> What I had live on 2026-08-16:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Object&lt;/th>
&lt;th>Status&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>SandboxAgent &lt;code>servicenow&lt;/code>&lt;/td>
&lt;td>&lt;code>Ready=True&lt;/code>, &lt;code>Accepted=True&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RemoteMCPServer &lt;code>servicenow-mcp&lt;/code>&lt;/td>
&lt;td>Accepted, &lt;strong>8&lt;/strong> tools, &lt;code>STREAMABLE_HTTP&lt;/code> &lt;code>http://servicenow-mcp.kagent:8084/mcp&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>MCP pod&lt;/td>
&lt;td>&lt;code>servicenow-mcp-7c6c455c65-kvnrq&lt;/code> &lt;code>1/1&lt;/code> Running, 0 restarts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ActorTemplate&lt;/td>
&lt;td>&lt;code>servicenow-f5f2dec1f2a81a41&lt;/code>, gvisor, Ready&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Snapshot prefix&lt;/td>
&lt;td>&lt;code>gs://ate-snapshots/kagent/servicenow&lt;/code> (rustfs)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Golden snapshot&lt;/td>
&lt;td>&lt;code>2026-08-16T15:45:24Z-HYSFX5R3DHWLRWYVP7YXOAF4SN&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/cli-live-status.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/cli-live-status.png" alt="Terminal showing SandboxAgent Ready, RemoteMCPServer Accepted with 8 tools, and the golden snapshot on rustfs" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live CLI, 2026-08-16. Nothing sensitive in frame.&lt;/em>&lt;/p>
&lt;h2 id="six-read-tools-two-write-tools">Six read tools, two write tools&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Kind&lt;/th>
&lt;th>What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>sn_whoami&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Which ServiceNow user the agent is acting as&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_list_incidents&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Active incidents, compact rows&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_get_incident&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>One incident by number or sys_id&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_search_incidents&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Encoded-query search&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_incident_summary&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Aggregated counts by field&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_list_requested_items&lt;/code>&lt;/td>
&lt;td>read&lt;/td>
&lt;td>Service catalog requested items&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_add_work_note&lt;/code>&lt;/td>
&lt;td>&lt;strong>write&lt;/strong>&lt;/td>
&lt;td>Appends a work note to an incident&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>sn_assign_incident&lt;/code>&lt;/td>
&lt;td>&lt;strong>write&lt;/strong>&lt;/td>
&lt;td>Sets an assignee&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is &lt;strong>no generic shell&lt;/strong> and no HTTP passthrough — you cannot ask this agent to hit an arbitrary Table API path. And critically: &lt;strong>no create, no close, no delete.&lt;/strong> An incident cannot be conjured or disappeared.&lt;/p>
&lt;h2 id="where-write-tools-belong">Where write tools belong&lt;/h2>
&lt;p>This is the part worth arguing about, so let me be direct about the position.&lt;/p>
&lt;p>Read-only agents are easy to trust and frequently useless. &amp;ldquo;Here are your 25 open tickets&amp;rdquo; is genuinely helpful, but the manager&amp;rsquo;s next sentence is always &lt;em>&amp;ldquo;okay, assign the payroll one to someone and note that we&amp;rsquo;re on it.&amp;rdquo;&lt;/em> If the agent can&amp;rsquo;t do that, you alt-tab into ServiceNow and the agent was a fancy report.&lt;/p>
&lt;p>So the two writes exist. What makes them defensible is not that they&amp;rsquo;re small — it&amp;rsquo;s that they&amp;rsquo;re &lt;strong>specific and reversible&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>sn_add_work_note&lt;/code> is &lt;strong>append-only&lt;/strong>. It cannot overwrite history. Worst case, someone gets a spurious note on a ticket, and the note is signed by the agent&amp;rsquo;s identity.&lt;/li>
&lt;li>&lt;code>sn_assign_incident&lt;/code> sets one field. Wrong assignee is an annoyance, fixed by setting it again.&lt;/li>
&lt;/ul>
&lt;p>Compare that to what I deliberately did &lt;strong>not&lt;/strong> build. A &lt;code>sn_close_incident&lt;/code> destroys the audit story — a closed ticket stops getting looked at, which is exactly the outcome a prompt injection would want. A generic &lt;code>sn_table_patch&lt;/code> is a shell with extra steps: any field, any table, including &lt;code>sys_user&lt;/code> roles.&lt;/p>
&lt;p>On top of that, the agent&amp;rsquo;s instructions require it to &lt;strong>ask before writing.&lt;/strong> That&amp;rsquo;s a soft control — a model can be talked out of a soft control, which is precisely why it&amp;rsquo;s the &lt;em>third&lt;/em> layer and not the only one. The hard controls are the tool catalog (there is no verb for &amp;ldquo;close&amp;rdquo;) and the ServiceNow account&amp;rsquo;s own role grants. The prompt-level &amp;ldquo;ask first&amp;rdquo; is there for the ordinary case where the manager and the agent are just working through a list, not for the adversarial one.&lt;/p>
&lt;p>The general rule I&amp;rsquo;d apply to any agent touching a system of record: &lt;strong>give it writes that a human can undo in one click, and nothing else.&lt;/strong>&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>Two questions through &lt;code>/api/a2a-sandboxes/kagent/servicenow&lt;/code>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/ui-chat-session.png" alt="Live servicenow chat: 25 active incidents broken into P1 Critical 13, P2 High 4, P3 Moderate 5, P5 Planning 3, with full incident tables and a needs-eyes unassigned section" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three tool calls: &lt;code>sn_whoami&lt;/code>, &lt;code>sn_incident_summary&lt;/code>, &lt;code>sn_list_incidents&lt;/code>.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What IT tickets are open right now? Give me a manager summary by priority.&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>Acting as &lt;strong>System Administrator (admin)&lt;/strong> on &lt;code>dev203166.service-now.com&lt;/code>. 25 active incidents on the current page:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Priority&lt;/th>
&lt;th style="text-align:right">Count&lt;/th>
&lt;th>Manager view&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>P1 – Critical&lt;/td>
&lt;td style="text-align:right">13&lt;/td>
&lt;td>Payroll server, DNS, wireless/network, VPN, Exchange, SAP apps&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P2 – High&lt;/td>
&lt;td style="text-align:right">4&lt;/td>
&lt;td>Network storage, SAP login, hotfix removal, VM launch&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P3 – Moderate&lt;/td>
&lt;td style="text-align:right">5&lt;/td>
&lt;td>Desk phone, email, service request, website JavaScript, SFA access&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>P5 – Planning&lt;/td>
&lt;td style="text-align:right">3&lt;/td>
&lt;td>Weather report, replacement iPhone, Oregon mail server&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Then the section that makes it a briefing rather than a dump — &lt;strong>needs eyes / unassigned&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>INC0007001&lt;/strong> — P1 Critical, New, unassigned: &lt;em>employee payroll application server is down&lt;/em>&lt;/li>
&lt;li>&lt;strong>INC0000046&lt;/strong> — P3 Moderate, New, unassigned: &lt;em>can&amp;rsquo;t access SFA software&lt;/em>&lt;/li>
&lt;li>&lt;strong>INC0000039&lt;/strong> — P5 Planning, New, unassigned: &lt;em>trouble getting to Oregon mail server&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>An unassigned P1 on a payroll server is exactly the thing that falls through a queue view, and it&amp;rsquo;s the first thing the agent surfaced.&lt;/p>
&lt;p>&lt;strong>The honest bit:&lt;/strong> &lt;code>sn_incident_summary&lt;/code> — the aggregate-count tool — &lt;strong>failed&lt;/strong> on this run. The agent said so in the answer (&amp;ldquo;the formal summary count tool failed, so priority totals below are from the returned incident list only&amp;rdquo;) and derived the buckets from the rows it actually had. It didn&amp;rsquo;t present a computed total as an API total. Same principle as the &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP agent saying &lt;em>unavailable&lt;/em>&lt;/a>: tell me where the number came from.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;What about the critical VPN and DNS ones?&amp;rdquo;&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Incident&lt;/th>
&lt;th>Short description&lt;/th>
&lt;th>State&lt;/th>
&lt;th>Assignee&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>INC0000015&lt;/td>
&lt;td>VPN client won&amp;rsquo;t launch since the last software update&lt;/td>
&lt;td>In Progress&lt;/td>
&lt;td>Don Goodliffe&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>INC0000016&lt;/td>
&lt;td>Rain is leaking on the main DNS server&lt;/td>
&lt;td>In Progress&lt;/td>
&lt;td>ITIL User&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Rain leaking on the DNS server is stock ServiceNow demo data, and I love it too much to swap for something plausible.&lt;/p>
&lt;p>Here&amp;rsquo;s the same session as a scroll:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/kagent-servicenow-live.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-servicenow-sandboxagent/kagent-servicenow-live.gif" alt="Animated scroll of the live servicenow chat session" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>One tell that this is a real sandboxed session: &lt;code>/api/a2a/kagent/servicenow&lt;/code> &lt;strong>404s&lt;/strong> — there is no &lt;code>Agent&lt;/code> CR — and the UI posts to &lt;code>/api/a2a-sandboxes/kagent/servicenow&lt;/code>. The card wears a &lt;em>Sandbox: Agent Substrate&lt;/em> badge.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the MCP image on the k3s node before the pod starts.&lt;/li>
&lt;li>The Vault path must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>No generic shell, no HTTP passthrough, no incident create / close / delete.&lt;/li>
&lt;li>The two write tools exist and the agent must ask first — that&amp;rsquo;s a prompt-level control backed by a tool-level one, not a substitute for it.&lt;/li>
&lt;li>&lt;code>sn_incident_summary&lt;/code> failed on the live run. Priority totals came from the returned rows.&lt;/li>
&lt;li>This is a personal developer instance with stock demo data, not production volume.&lt;/li>
&lt;li>Never commit the ServiceNow username or password &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Full demo — manifests, the FastMCP server, security notes, runbooks, and the live report: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/service-now-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / service-now-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>Asking My Home FortiGate Questions: A gVisor SandboxAgent on kagent</title><link>https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/</link><pubDate>Mon, 17 Aug 2026 08:15:00 -0400</pubDate><guid>https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/</guid><description>&lt;p>My &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP&lt;/a> budget agents read cloud bills. This one reads something with considerably more consequence in my house: &lt;strong>the firewall my family&amp;rsquo;s internet goes through.&lt;/strong>&lt;/p>
&lt;p>&lt;code>fw-maniak-hq&lt;/code> is a FortiGate 80F at &lt;code>172.16.10.1&lt;/code>. Real traffic, real policies, and — importantly — policies I wrote months ago and no longer remember the details of. That last part is what makes a home firewall a &lt;em>better&lt;/em> test subject than a pristine lab box. The interesting questions aren&amp;rsquo;t &amp;ldquo;is it up,&amp;rdquo; they&amp;rsquo;re archaeological:&lt;/p>
&lt;blockquote>
&lt;p>What is fw-maniak-hq running, and which WAN is up?&lt;/p>
&lt;p>What&amp;rsquo;s the YouTube policy?&lt;/p>
&lt;/blockquote>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with five SandboxAgent cards including kagent/fortigate" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Isolated sandboxes, not plain Agents — &lt;code>kagent/fortigate&lt;/code> on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation, same as every other pod. Fine for a cluster helper.&lt;/p>
&lt;p>This one holds a REST token for the device that gates my entire household&amp;rsquo;s network. The model gets a filesystem, memory, and a live network for the whole chat. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel is the wall between the model session and my Viper/k3s host. Tools call FortiOS through the MCP pod; the REST token stays in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker. The next message restores the same session instead of booting a new container.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Tradeoff on this lab:&lt;/strong> nested gVisor on dockerized k3s, and snapshots are in-cluster rustfs today (&lt;code>gs://&lt;/code> is a URI prefix only), not GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;fortigate-mcp&amp;#34;]
 mcp[&amp;#34;fortigate-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 fg[&amp;#34;FortiGate 80F&amp;lt;br/&amp;gt;172.16.10.1&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/fortigate&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; fg
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The REST token goes Vault → External Secrets Operator → &lt;strong>the MCP pod&lt;/strong>. The actor calls tools over MCP and never holds the token. If a session gets confused by something it read, the credential is still on the far side of the sandbox wall.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Box&lt;/td>
&lt;td>FortiGate &lt;strong>80F&lt;/strong> · &lt;code>fw-maniak-hq&lt;/code> · &lt;code>172.16.10.1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FortiOS&lt;/td>
&lt;td>v7.4.11 build 2878 · VDOM &lt;code>root&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 always writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that shape; &lt;strong>0.0.12&lt;/strong> does not. Do not &amp;ldquo;upgrade to fix&amp;rdquo; a &lt;code>Ready=False&lt;/code> agent — a pin mismatch is a CRD problem.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>GitOps for this one lives in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> (&lt;code>platform/kagent-ai/fortigate-*.yaml&lt;/code>, &lt;code>images/fortigate-mcp/&lt;/code>, &lt;code>docs/fortigate-agent.md&lt;/code>) — the demo folder in the substrate repo is the &lt;strong>live-run record&lt;/strong>. The shape is the same as the others:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>FortiGate side.&lt;/strong> Create a REST API admin with a read-mostly profile, restricted to the trusted host that the MCP pod egresses from. Generate the token once.&lt;/li>
&lt;li>&lt;strong>Vault.&lt;/strong> Path &lt;code>secret/platform/fortigate&lt;/code>. ESO syncs it into a Kubernetes Secret the MCP pod consumes. Git holds the mapping only.&lt;/li>
&lt;li>&lt;strong>Image.&lt;/strong> Build &lt;code>fortigate-mcp:dev&lt;/code>, &lt;code>ctr images import&lt;/code> onto the k3s node. Dockerized k3s can&amp;rsquo;t see host Docker images; &lt;code>imagePullPolicy: IfNotPresent&lt;/code> is deliberate.&lt;/li>
&lt;li>&lt;strong>Apply&lt;/strong> the SandboxAgent, RemoteMCPServer, ExternalSecret, and skills ConfigMap.&lt;/li>
&lt;/ol>
&lt;p>Verified live on 2026-08-16, and this is the whole proof surface:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME READY ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/fortigate True True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME PROTOCOL URL ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/fortigate-mcp STREAMABLE_HTTP http://fortigate-mcp.kagent:8084/mcp True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pod/fortigate-mcp-745d4c9ff5-bxjvc 1/1 Running 0 19h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME CLASS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actortemplate.ate.dev/fortigate-b8bc65944f9bc4df gvisor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">location: gs://ate-snapshots/kagent/fortigate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goldenSnapshot: gs://ate-snapshots/kagent/fortigate/2bcc7a8b-.../2026-08-16T02:45:18Z-R7ZXMSV6CEC2D4NVN4XEX5UDO3
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">phase: Ready
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Ready=True&lt;/code> with a &lt;code>goldenSnapshot&lt;/code> means Substrate booted a golden actor, checkpointed it, and new chats restore from that image rather than cold-starting.&lt;/p>
&lt;h2 id="the-tool-catalog">The tool catalog&lt;/h2>
&lt;p>This agent is the widest of the five demos — 22 tools, because a firewall has a lot of surfaces worth reading:&lt;/p>
&lt;p>&lt;strong>Reads.&lt;/strong> &lt;code>fg_system_status&lt;/code>, &lt;code>fg_resource_usage&lt;/code>, &lt;code>fg_list_interfaces&lt;/code>, &lt;code>fg_interface_stats&lt;/code>, &lt;code>fg_list_policies&lt;/code>, &lt;code>fg_get_policy&lt;/code>, &lt;code>fg_policy_stats&lt;/code>, &lt;code>fg_list_addresses&lt;/code>, &lt;code>fg_list_addrgrp&lt;/code>, &lt;code>fg_list_services&lt;/code>, &lt;code>fg_list_routes&lt;/code>, &lt;code>fg_list_static_routes&lt;/code>, &lt;code>fg_vpn_status&lt;/code>, &lt;code>fg_dhcp_leases&lt;/code>, &lt;code>fg_list_vips&lt;/code>, &lt;code>fg_log_state&lt;/code>, &lt;code>fg_current_admins&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Writes.&lt;/strong> &lt;code>fg_create_address&lt;/code>, &lt;code>fg_update_address&lt;/code>, &lt;code>fg_create_policy&lt;/code>, &lt;code>fg_set_policy_status&lt;/code>, &lt;code>fg_update_policy_comment&lt;/code>.&lt;/p>
&lt;p>And, more importantly, what is &lt;strong>absent&lt;/strong>: there is no generic &amp;ldquo;run any FortiOS CLI&amp;rdquo; tool, no tmsh-equivalent, no config backup, no firmware operation, and no delete of anything. The write tools that exist are additive or single-field — create an address object, flip a policy&amp;rsquo;s enable flag, edit a comment. The agent has to &lt;strong>ask before using any of them.&lt;/strong>&lt;/p>
&lt;p>That&amp;rsquo;s the same position I took with the &lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow agent&amp;rsquo;s two write tools&lt;/a>: give it writes a human can undo in one click, and nothing else. On a firewall the stakes are higher, so the &amp;ldquo;nothing else&amp;rdquo; list is longer — a &lt;code>fg_delete_policy&lt;/code> on a home gateway is a self-inflicted outage waiting for a bad prompt.&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>Two questions through &lt;code>/api/a2a-sandboxes/kagent/fortigate&lt;/code>, 2026-08-16 evening.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-chat-session.png" alt="Live fortigate chat: FortiGate 80F, serial FGT80FTK22061709, FortiOS v7.4.11 build 2878, VDOM root, wan1 down with 0.0.0.0 and wan2 up with 24.141.221.254/20, then two YouTube policies with hit counts" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three tool calls on Q1, four on Q2. The right-hand pane shows the full 22-tool catalog.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What is fw-maniak-hq running, and which WAN is up?&amp;rdquo;&lt;/strong> (~6.6s, tools: &lt;code>fg_system_status&lt;/code>, &lt;code>fg_list_interfaces&lt;/code>, &lt;code>fg_interface_stats&lt;/code>)&lt;/p>
&lt;ul>
&lt;li>FortiGate &lt;strong>80F&lt;/strong> / FGT80F, serial &lt;code>FGT80FTK22061709&lt;/code>&lt;/li>
&lt;li>FortiOS &lt;strong>v7.4.11&lt;/strong> build 2878, VDOM &lt;code>root&lt;/code>&lt;/li>
&lt;li>&lt;strong>wan1: down&lt;/strong>, DHCP, IP &lt;code>0.0.0.0&lt;/code>&lt;/li>
&lt;li>&lt;strong>wan2: up&lt;/strong>, DHCP, IP &lt;code>24.141.221.254/20&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Six and a half seconds for hardware, firmware, and WAN failover state. Worth noting it correctly read &lt;code>0.0.0.0&lt;/code> as &lt;em>down&lt;/em> rather than reporting an interface that exists as an interface that works.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;What&amp;rsquo;s the YouTube policy?&amp;rdquo;&lt;/strong> (~12.0s, tools: &lt;code>fg_list_policies&lt;/code>, &lt;code>fg_get_policy&lt;/code> ×2, &lt;code>fg_policy_stats&lt;/code>)&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>ID&lt;/th>
&lt;th>Name&lt;/th>
&lt;th>Source&lt;/th>
&lt;th>Schedule&lt;/th>
&lt;th style="text-align:right">Hits&lt;/th>
&lt;th style="text-align:right">Active sessions&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>&lt;code>Allow-YouTube-Whitelist&lt;/code>&lt;/td>
&lt;td>&lt;code>Grp-YouTube-Allowed&lt;/code>&lt;/td>
&lt;td>always&lt;/td>
&lt;td style="text-align:right">3,007,844&lt;/td>
&lt;td style="text-align:right">154&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>&lt;code>Allow-YouTube-MasterBR-Night&lt;/code>&lt;/td>
&lt;td>&lt;code>YT-AppleTV-Master-122&lt;/code>&lt;/td>
&lt;td>&lt;code>Allow-YT-MasterBR-8pm-2am&lt;/code>&lt;/td>
&lt;td style="text-align:right">18,902&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Both from &lt;code>corp&lt;/code> → &lt;code>BELL_35&lt;/code> / &lt;code>wan2&lt;/code>, action accept, NAT on, logging all. Policy 8 even carried its own documentation in the comment field: &lt;em>&amp;ldquo;Whitelist: these devices may use YouTube (and everything). Add devices to Grp-YouTube-Allowed.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>Then the answer I actually needed: &lt;strong>there is no separate YouTube block policy&lt;/strong> in the returned list. The allow-list is doing the work by being narrow, not by pairing with an explicit deny — and the agent said that rather than assuming a deny must exist somewhere because the setup &amp;ldquo;looks like&amp;rdquo; a whitelist pattern.&lt;/p>
&lt;h2 id="what-this-demo-is-actually-good-at">What this demo is actually good at&lt;/h2>
&lt;p>Two things stand out after using it.&lt;/p>
&lt;p>&lt;strong>It reads intent, not just config.&lt;/strong> &lt;code>fg_policy_stats&lt;/code> turning &amp;ldquo;policy 8&amp;rdquo; into &lt;em>3,007,844 hits, 154 sessions right now&lt;/em> is the difference between reading a rule and knowing whether the rule matters. Policy 7 has 18,902 lifetime hits and zero active sessions — its schedule window (&lt;code>8pm-2am&lt;/code>) wasn&amp;rsquo;t open. Neither of those facts is in the policy definition.&lt;/p>
&lt;p>&lt;strong>It&amp;rsquo;s honest about what it can see.&lt;/strong> Twice in one session it declined to fill a gap: no YouTube deny policy in the compact list, and &lt;code>wan1&lt;/code> reported as down rather than glossed. Same principle as &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">the GCP agent saying &lt;em>unavailable&lt;/em>&lt;/a> rather than inventing spend. On a firewall, a confidently hallucinated policy is worse than no answer — you&amp;rsquo;d go make a change based on a rule that doesn&amp;rsquo;t exist.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the MCP image on the k3s node before the pod starts.&lt;/li>
&lt;li>The Vault path &lt;code>secret/platform/fortigate&lt;/code> must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>The kagent UI at &lt;code>http://172.16.10.135:30500/&lt;/code> is &lt;strong>LAN-only&lt;/strong>. This isn&amp;rsquo;t published.&lt;/li>
&lt;li>No generic FortiOS CLI tool. No delete, no config backup, no firmware operation.&lt;/li>
&lt;li>Writes exist (&lt;code>fg_create_policy&lt;/code>, &lt;code>fg_set_policy_status&lt;/code>, …) and the agent must ask first — a prompt-level control backed by a tool-level one, not a replacement for it.&lt;/li>
&lt;li>Snapshots are in-cluster rustfs. &lt;code>gs://&lt;/code> is a prefix only.&lt;/li>
&lt;li>Never commit the FortiGate REST token or Vault secret &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Live-run record: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/fortigate-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / fortigate-sandbox-agent&lt;/a>. Manifests and the MCP image are in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> — see &lt;a href="https://github.com/sebbycorp/k8s-viper/blob/main/docs/fortigate-agent.md">docs/fortigate-agent.md&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>My &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP&lt;/a> budget agents read cloud bills. This one reads something with considerably more consequence in my house: &lt;strong>the firewall my family&amp;rsquo;s internet goes through.&lt;/strong>&lt;/p>
&lt;p>&lt;code>fw-maniak-hq&lt;/code> is a FortiGate 80F at &lt;code>172.16.10.1&lt;/code>. Real traffic, real policies, and — importantly — policies I wrote months ago and no longer remember the details of. That last part is what makes a home firewall a &lt;em>better&lt;/em> test subject than a pristine lab box. The interesting questions aren&amp;rsquo;t &amp;ldquo;is it up,&amp;rdquo; they&amp;rsquo;re archaeological:&lt;/p>
&lt;blockquote>
&lt;p>What is fw-maniak-hq running, and which WAN is up?&lt;/p>
&lt;p>What&amp;rsquo;s the YouTube policy?&lt;/p>
&lt;/blockquote>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with five SandboxAgent cards including kagent/fortigate" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Isolated sandboxes, not plain Agents — &lt;code>kagent/fortigate&lt;/code> on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation, same as every other pod. Fine for a cluster helper.&lt;/p>
&lt;p>This one holds a REST token for the device that gates my entire household&amp;rsquo;s network. The model gets a filesystem, memory, and a live network for the whole chat. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel is the wall between the model session and my Viper/k3s host. Tools call FortiOS through the MCP pod; the REST token stays in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker. The next message restores the same session instead of booting a new container.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Tradeoff on this lab:&lt;/strong> nested gVisor on dockerized k3s, and snapshots are in-cluster rustfs today (&lt;code>gs://&lt;/code> is a URI prefix only), not GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;fortigate-mcp&amp;#34;]
 mcp[&amp;#34;fortigate-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 fg[&amp;#34;FortiGate 80F&amp;lt;br/&amp;gt;172.16.10.1&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/fortigate&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; fg
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The REST token goes Vault → External Secrets Operator → &lt;strong>the MCP pod&lt;/strong>. The actor calls tools over MCP and never holds the token. If a session gets confused by something it read, the credential is still on the far side of the sandbox wall.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Box&lt;/td>
&lt;td>FortiGate &lt;strong>80F&lt;/strong> · &lt;code>fw-maniak-hq&lt;/code> · &lt;code>172.16.10.1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FortiOS&lt;/td>
&lt;td>v7.4.11 build 2878 · VDOM &lt;code>root&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 always writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that shape; &lt;strong>0.0.12&lt;/strong> does not. Do not &amp;ldquo;upgrade to fix&amp;rdquo; a &lt;code>Ready=False&lt;/code> agent — a pin mismatch is a CRD problem.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>GitOps for this one lives in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> (&lt;code>platform/kagent-ai/fortigate-*.yaml&lt;/code>, &lt;code>images/fortigate-mcp/&lt;/code>, &lt;code>docs/fortigate-agent.md&lt;/code>) — the demo folder in the substrate repo is the &lt;strong>live-run record&lt;/strong>. The shape is the same as the others:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>FortiGate side.&lt;/strong> Create a REST API admin with a read-mostly profile, restricted to the trusted host that the MCP pod egresses from. Generate the token once.&lt;/li>
&lt;li>&lt;strong>Vault.&lt;/strong> Path &lt;code>secret/platform/fortigate&lt;/code>. ESO syncs it into a Kubernetes Secret the MCP pod consumes. Git holds the mapping only.&lt;/li>
&lt;li>&lt;strong>Image.&lt;/strong> Build &lt;code>fortigate-mcp:dev&lt;/code>, &lt;code>ctr images import&lt;/code> onto the k3s node. Dockerized k3s can&amp;rsquo;t see host Docker images; &lt;code>imagePullPolicy: IfNotPresent&lt;/code> is deliberate.&lt;/li>
&lt;li>&lt;strong>Apply&lt;/strong> the SandboxAgent, RemoteMCPServer, ExternalSecret, and skills ConfigMap.&lt;/li>
&lt;/ol>
&lt;p>Verified live on 2026-08-16, and this is the whole proof surface:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME READY ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/fortigate True True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME PROTOCOL URL ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/fortigate-mcp STREAMABLE_HTTP http://fortigate-mcp.kagent:8084/mcp True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pod/fortigate-mcp-745d4c9ff5-bxjvc 1/1 Running 0 19h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME CLASS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actortemplate.ate.dev/fortigate-b8bc65944f9bc4df gvisor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">location: gs://ate-snapshots/kagent/fortigate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goldenSnapshot: gs://ate-snapshots/kagent/fortigate/2bcc7a8b-.../2026-08-16T02:45:18Z-R7ZXMSV6CEC2D4NVN4XEX5UDO3
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">phase: Ready
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Ready=True&lt;/code> with a &lt;code>goldenSnapshot&lt;/code> means Substrate booted a golden actor, checkpointed it, and new chats restore from that image rather than cold-starting.&lt;/p>
&lt;h2 id="the-tool-catalog">The tool catalog&lt;/h2>
&lt;p>This agent is the widest of the five demos — 22 tools, because a firewall has a lot of surfaces worth reading:&lt;/p>
&lt;p>&lt;strong>Reads.&lt;/strong> &lt;code>fg_system_status&lt;/code>, &lt;code>fg_resource_usage&lt;/code>, &lt;code>fg_list_interfaces&lt;/code>, &lt;code>fg_interface_stats&lt;/code>, &lt;code>fg_list_policies&lt;/code>, &lt;code>fg_get_policy&lt;/code>, &lt;code>fg_policy_stats&lt;/code>, &lt;code>fg_list_addresses&lt;/code>, &lt;code>fg_list_addrgrp&lt;/code>, &lt;code>fg_list_services&lt;/code>, &lt;code>fg_list_routes&lt;/code>, &lt;code>fg_list_static_routes&lt;/code>, &lt;code>fg_vpn_status&lt;/code>, &lt;code>fg_dhcp_leases&lt;/code>, &lt;code>fg_list_vips&lt;/code>, &lt;code>fg_log_state&lt;/code>, &lt;code>fg_current_admins&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Writes.&lt;/strong> &lt;code>fg_create_address&lt;/code>, &lt;code>fg_update_address&lt;/code>, &lt;code>fg_create_policy&lt;/code>, &lt;code>fg_set_policy_status&lt;/code>, &lt;code>fg_update_policy_comment&lt;/code>.&lt;/p>
&lt;p>And, more importantly, what is &lt;strong>absent&lt;/strong>: there is no generic &amp;ldquo;run any FortiOS CLI&amp;rdquo; tool, no tmsh-equivalent, no config backup, no firmware operation, and no delete of anything. The write tools that exist are additive or single-field — create an address object, flip a policy&amp;rsquo;s enable flag, edit a comment. The agent has to &lt;strong>ask before using any of them.&lt;/strong>&lt;/p>
&lt;p>That&amp;rsquo;s the same position I took with the &lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow agent&amp;rsquo;s two write tools&lt;/a>: give it writes a human can undo in one click, and nothing else. On a firewall the stakes are higher, so the &amp;ldquo;nothing else&amp;rdquo; list is longer — a &lt;code>fg_delete_policy&lt;/code> on a home gateway is a self-inflicted outage waiting for a bad prompt.&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>Two questions through &lt;code>/api/a2a-sandboxes/kagent/fortigate&lt;/code>, 2026-08-16 evening.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-fortigate-sandboxagent/ui-chat-session.png" alt="Live fortigate chat: FortiGate 80F, serial FGT80FTK22061709, FortiOS v7.4.11 build 2878, VDOM root, wan1 down with 0.0.0.0 and wan2 up with 24.141.221.254/20, then two YouTube policies with hit counts" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three tool calls on Q1, four on Q2. The right-hand pane shows the full 22-tool catalog.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What is fw-maniak-hq running, and which WAN is up?&amp;rdquo;&lt;/strong> (~6.6s, tools: &lt;code>fg_system_status&lt;/code>, &lt;code>fg_list_interfaces&lt;/code>, &lt;code>fg_interface_stats&lt;/code>)&lt;/p>
&lt;ul>
&lt;li>FortiGate &lt;strong>80F&lt;/strong> / FGT80F, serial &lt;code>FGT80FTK22061709&lt;/code>&lt;/li>
&lt;li>FortiOS &lt;strong>v7.4.11&lt;/strong> build 2878, VDOM &lt;code>root&lt;/code>&lt;/li>
&lt;li>&lt;strong>wan1: down&lt;/strong>, DHCP, IP &lt;code>0.0.0.0&lt;/code>&lt;/li>
&lt;li>&lt;strong>wan2: up&lt;/strong>, DHCP, IP &lt;code>24.141.221.254/20&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Six and a half seconds for hardware, firmware, and WAN failover state. Worth noting it correctly read &lt;code>0.0.0.0&lt;/code> as &lt;em>down&lt;/em> rather than reporting an interface that exists as an interface that works.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;What&amp;rsquo;s the YouTube policy?&amp;rdquo;&lt;/strong> (~12.0s, tools: &lt;code>fg_list_policies&lt;/code>, &lt;code>fg_get_policy&lt;/code> ×2, &lt;code>fg_policy_stats&lt;/code>)&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>ID&lt;/th>
&lt;th>Name&lt;/th>
&lt;th>Source&lt;/th>
&lt;th>Schedule&lt;/th>
&lt;th style="text-align:right">Hits&lt;/th>
&lt;th style="text-align:right">Active sessions&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>&lt;code>Allow-YouTube-Whitelist&lt;/code>&lt;/td>
&lt;td>&lt;code>Grp-YouTube-Allowed&lt;/code>&lt;/td>
&lt;td>always&lt;/td>
&lt;td style="text-align:right">3,007,844&lt;/td>
&lt;td style="text-align:right">154&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>&lt;code>Allow-YouTube-MasterBR-Night&lt;/code>&lt;/td>
&lt;td>&lt;code>YT-AppleTV-Master-122&lt;/code>&lt;/td>
&lt;td>&lt;code>Allow-YT-MasterBR-8pm-2am&lt;/code>&lt;/td>
&lt;td style="text-align:right">18,902&lt;/td>
&lt;td style="text-align:right">0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Both from &lt;code>corp&lt;/code> → &lt;code>BELL_35&lt;/code> / &lt;code>wan2&lt;/code>, action accept, NAT on, logging all. Policy 8 even carried its own documentation in the comment field: &lt;em>&amp;ldquo;Whitelist: these devices may use YouTube (and everything). Add devices to Grp-YouTube-Allowed.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>Then the answer I actually needed: &lt;strong>there is no separate YouTube block policy&lt;/strong> in the returned list. The allow-list is doing the work by being narrow, not by pairing with an explicit deny — and the agent said that rather than assuming a deny must exist somewhere because the setup &amp;ldquo;looks like&amp;rdquo; a whitelist pattern.&lt;/p>
&lt;h2 id="what-this-demo-is-actually-good-at">What this demo is actually good at&lt;/h2>
&lt;p>Two things stand out after using it.&lt;/p>
&lt;p>&lt;strong>It reads intent, not just config.&lt;/strong> &lt;code>fg_policy_stats&lt;/code> turning &amp;ldquo;policy 8&amp;rdquo; into &lt;em>3,007,844 hits, 154 sessions right now&lt;/em> is the difference between reading a rule and knowing whether the rule matters. Policy 7 has 18,902 lifetime hits and zero active sessions — its schedule window (&lt;code>8pm-2am&lt;/code>) wasn&amp;rsquo;t open. Neither of those facts is in the policy definition.&lt;/p>
&lt;p>&lt;strong>It&amp;rsquo;s honest about what it can see.&lt;/strong> Twice in one session it declined to fill a gap: no YouTube deny policy in the compact list, and &lt;code>wan1&lt;/code> reported as down rather than glossed. Same principle as &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">the GCP agent saying &lt;em>unavailable&lt;/em>&lt;/a> rather than inventing spend. On a firewall, a confidently hallucinated policy is worse than no answer — you&amp;rsquo;d go make a change based on a rule that doesn&amp;rsquo;t exist.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the MCP image on the k3s node before the pod starts.&lt;/li>
&lt;li>The Vault path &lt;code>secret/platform/fortigate&lt;/code> must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>The kagent UI at &lt;code>http://172.16.10.135:30500/&lt;/code> is &lt;strong>LAN-only&lt;/strong>. This isn&amp;rsquo;t published.&lt;/li>
&lt;li>No generic FortiOS CLI tool. No delete, no config backup, no firmware operation.&lt;/li>
&lt;li>Writes exist (&lt;code>fg_create_policy&lt;/code>, &lt;code>fg_set_policy_status&lt;/code>, …) and the agent must ask first — a prompt-level control backed by a tool-level one, not a replacement for it.&lt;/li>
&lt;li>Snapshots are in-cluster rustfs. &lt;code>gs://&lt;/code> is a prefix only.&lt;/li>
&lt;li>Never commit the FortiGate REST token or Vault secret &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/">F5 BIG-IP&lt;/a>&lt;/strong> — 19 VIPs, 18 tool calls in one turn, and zero write tools.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Live-run record: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/fortigate-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / fortigate-sandbox-agent&lt;/a>. Manifests and the MCP image are in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> — see &lt;a href="https://github.com/sebbycorp/k8s-viper/blob/main/docs/fortigate-agent.md">docs/fortigate-agent.md&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>An F5 BIG-IP SandboxAgent on kagent: 19 VIPs, 2 Up, and 18 Tool Calls to Find Out Why</title><link>https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/</link><pubDate>Mon, 17 Aug 2026 08:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-08-17-f5-bigip-sandboxagent-kagent-howto/</guid><description>&lt;p>The other four agents in this series answer &lt;em>what is the state of things&lt;/em> — &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS spend&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP quota&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">open tickets&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">firewall policy&lt;/a>. This one answers something different, and it&amp;rsquo;s the question I care about most on a load balancer:&lt;/p>
&lt;blockquote>
&lt;p>Which VIPs are down, and &lt;strong>why&lt;/strong>?&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;Down&amp;rdquo; is a lookup. &amp;ldquo;Why&amp;rdquo; is a fan-out — you have to walk from each offline virtual server to its pool, then to that pool&amp;rsquo;s members, and read the reason string. On my lab BIG-IP that turned into &lt;strong>18 tool calls in a single turn&lt;/strong>, and the agent did the walk itself.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with six SandboxAgent cards including kagent/f5-bigip" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-17. Isolated sandboxes, not plain Agents — &lt;code>kagent/f5-bigip&lt;/code> on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation. Fine for a cluster helper.&lt;/p>
&lt;p>This one holds credentials for the box that fronts every service in my lab. The model gets a filesystem, memory, and a network for the whole chat. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel sits between the model session and my Viper/k3s host. Tools call iControl REST through the MCP pod; the BIG-IP password stays in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker. The next message restores that session.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Tradeoff on this lab:&lt;/strong> nested gVisor on dockerized k3s, and snapshots are in-cluster rustfs today (&lt;code>gs://&lt;/code> is a URI prefix only), not GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;f5-bigip-mcp&amp;#34;]
 mcp[&amp;#34;f5-bigip-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 f5[&amp;#34;BIG-IP&amp;lt;br/&amp;gt;172.16.10.10&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/f5-bigip&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; f5
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>Vault → ESO → &lt;strong>the MCP pod&lt;/strong>. Not the actor. Same shape as the other four demos, and the reason is the same: the part of the system running the model and parsing untrusted output should never be the part holding the credential.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Box&lt;/td>
&lt;td>F5 BIG-IP · &lt;code>https://172.16.10.10&lt;/code> · LAN-only, self-signed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 always writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that shape; &lt;strong>0.0.12&lt;/strong> does not. A &lt;code>Ready=False&lt;/code> agent after a version bump is a CRD pin problem, not a reason to bump further.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>GitOps lives in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> (&lt;code>platform/kagent-ai/f5-bigip-*.yaml&lt;/code>, &lt;code>images/f5-bigip-mcp/&lt;/code>, &lt;code>docs/f5-bigip-agent.md&lt;/code>); the demo folder is the live-run record.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>BIG-IP side.&lt;/strong> A dedicated account with a read-only role. No &lt;code>tmsh&lt;/code> shell access.&lt;/li>
&lt;li>&lt;strong>Vault.&lt;/strong> Path &lt;code>secret/platform/f5-bigip&lt;/code>, keys &lt;code>host&lt;/code>, &lt;code>username&lt;/code>, &lt;code>password&lt;/code>. ESO syncs it to the MCP pod; git holds the mapping only.&lt;/li>
&lt;li>&lt;strong>Image.&lt;/strong> Build &lt;code>f5-bigip-mcp:dev&lt;/code> and &lt;code>ctr images import&lt;/code> it onto the k3s node.&lt;/li>
&lt;li>&lt;strong>Apply&lt;/strong> the SandboxAgent, RemoteMCPServer, and ExternalSecret.&lt;/li>
&lt;/ol>
&lt;p>Live on 2026-08-17 — the whole proof surface, with nothing sensitive in it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME READY ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/f5-bigip True True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME PROTOCOL URL ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/f5-bigip-mcp STREAMABLE_HTTP http://f5-bigip-mcp.kagent:8084/mcp True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME STORETYPE STORE STATUS READY
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">externalsecret.../f5-bigip-mcp ClusterSecretStore vault-backend SecretSynced True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pod/f5-bigip-mcp-7f75b47b78-mdblb 1/1 Running 0 17m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">image: f5-bigip-mcp:dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME CLASS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actortemplate.ate.dev/f5-bigip-3adfcbf7c448a873 gvisor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">location: gs://ate-snapshots/kagent/f5-bigip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goldenSnapshot: gs://ate-snapshots/kagent/f5-bigip/2b9f5b6a-.../2026-08-17T14:32:42Z-23PD7HXZ5JL7OO7RNHUWZ5YOWS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">phase: Ready
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="six-tools-zero-writes">Six tools, zero writes&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>What it reads&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>f5_system&lt;/code>&lt;/td>
&lt;td>Product, version, build&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_list_vips&lt;/code>&lt;/td>
&lt;td>Virtual servers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_vip_status&lt;/code>&lt;/td>
&lt;td>One virtual server&amp;rsquo;s availability and enabled state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_list_pools&lt;/code>&lt;/td>
&lt;td>Pools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_pool_status&lt;/code>&lt;/td>
&lt;td>One pool&amp;rsquo;s availability, reason string, and member states&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_vip_brief&lt;/code>&lt;/td>
&lt;td>Composed VIP → destination → pool → availability rollup&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s it. &lt;strong>No write tools exist at all&lt;/strong> — not create, not delete, not disable, not &amp;ldquo;modify this monitor.&amp;rdquo; There is also no generic &amp;ldquo;call any iControl path&amp;rdquo; tool, which is the important one: a passthrough would let anyone with chat access reach every endpoint the account can, and read-only-on-the-account would be the only remaining control.&lt;/p>
&lt;p>This is a different call than I made for &lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow&lt;/a>, where a few narrow, reversible writes earn their place. A load balancer is the wrong place to be casual: disabling the wrong VIP is an instant outage for every service behind it, and there&amp;rsquo;s no cheap undo when the thing you broke is the path your monitoring travels over. So this agent gets to observe and nothing else.&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>Two questions through &lt;code>/api/a2a-sandboxes/kagent/f5-bigip&lt;/code>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-chat-session.png" alt="Live f5-bigip chat: BIG-IP responded but product, version and build are null; two available VIPs listed with destinations and pools; 19 total VIPs checked with the rest offline; second answer shows 18 tool calls and the pool reason string" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-17. Two tool calls on Q1, &lt;strong>18&lt;/strong> on Q2. The 17-row down table continues below the fold.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What is this BIG-IP running, and which VIPs are up?&amp;rdquo;&lt;/strong> (~12.6s, tools: &lt;code>f5_system&lt;/code>, &lt;code>f5_vip_brief&lt;/code>)&lt;/p>
&lt;p>&lt;code>f5_system&lt;/code> reached &lt;code>https://172.16.10.10&lt;/code> and returned &lt;code>ok: true&lt;/code> — but the identity fields came back empty. Product &lt;code>null&lt;/code>. Version &lt;code>null&lt;/code>. Build &lt;code>null&lt;/code>.&lt;/p>
&lt;p>The agent reported exactly that. It did &lt;strong>not&lt;/strong> substitute a TMOS version from anywhere, and it didn&amp;rsquo;t decide the box was unreachable either — it separated &amp;ldquo;the API answered&amp;rdquo; from &amp;ldquo;the answer was empty,&amp;rdquo; which are genuinely different failure modes on iControl.&lt;/p>
&lt;p>Then &lt;code>f5_vip_brief&lt;/code> returned &lt;strong>19&lt;/strong> virtual servers, of which two were available:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>VIP&lt;/th>
&lt;th>Destination&lt;/th>
&lt;th>Pool&lt;/th>
&lt;th>Availability&lt;/th>
&lt;th>Enabled&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>k8s_iceman_argocd_vs&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.60:443&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/k8s_iceman_argocd_pool&lt;/code>&lt;/td>
&lt;td>&lt;code>available&lt;/code>&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>k8s_iceman_kagent_vs&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.62:8080&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/k8s_iceman_kagent_pool&lt;/code>&lt;/td>
&lt;td>&lt;code>available&lt;/code>&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The other 17: &lt;code>offline&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;Which VIPs are down, and why (pool members)?&amp;rdquo;&lt;/strong> (~36.4s, tools: &lt;code>f5_system&lt;/code>, &lt;code>f5_vip_brief&lt;/code>, &lt;code>f5_pool_status&lt;/code> ×16)&lt;/p>
&lt;p>This is where a single-shot tool wouldn&amp;rsquo;t have helped. The agent walked each offline VIP to its pool and pulled the status individually — sixteen &lt;code>f5_pool_status&lt;/code> calls — and every one came back the same way:&lt;/p>
&lt;ul>
&lt;li>Pool availability &lt;code>offline&lt;/code>, pool state &lt;code>enabled&lt;/code>&lt;/li>
&lt;li>Reason: &lt;strong>&amp;ldquo;The children pool member(s) are down&amp;rdquo;&lt;/strong>&lt;/li>
&lt;li>Members &lt;code>state: down&lt;/code>, &lt;code>session: monitor-enabled&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>And the conclusion that actually matters: &lt;strong>all 17 offline VIPs were still &lt;code>enabled&lt;/code>.&lt;/strong> Nobody administratively disabled anything. The VIPs are fine; the backends are gone. On my lab that&amp;rsquo;s the expected story — those pools point at Talos and k3s node ports across clusters I&amp;rsquo;d shut down — but &amp;ldquo;config is fine, backends are dead&amp;rdquo; versus &amp;ldquo;someone disabled the VIP&amp;rdquo; is the entire first branch of a load balancer triage tree, and the agent got there on its own.&lt;/p>
&lt;p>A sample of the 17, with the down members it named:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>VIP&lt;/th>
&lt;th>Destination&lt;/th>
&lt;th>Pool&lt;/th>
&lt;th>Down members&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agentgateway-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.30:8080&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/agentgetway-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.144:30344&lt;/code>, &lt;code>.144:30513&lt;/code>, &lt;code>.148:30344&lt;/code>, &lt;code>.148:30513&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>k8s_iceman_vault_vs&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.61:8200&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/k8s_iceman_vault_pool&lt;/code>&lt;/td>
&lt;td>&lt;code>talos-cp:30820&lt;/code>, &lt;code>talos-worker:30820&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.36:80&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/kagent-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.144:31438&lt;/code>, &lt;code>.144:32002&lt;/code>, &lt;code>.148:31438&lt;/code>, &lt;code>.148:32002&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vs_mcp_gateway&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.123:8090&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/pool_mcp_gateway&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.130:30168&lt;/code>, &lt;code>.132:30168&lt;/code>, &lt;code>.133:30168&lt;/code>, &lt;code>.136:30168&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>webui-https&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.31:443&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/webui-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.144:30694&lt;/code>, &lt;code>172.16.10.148:30694&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Final tally: &lt;strong>2 available, 17 offline, 19 total.&lt;/strong>&lt;/p>
&lt;p>(Yes, one of my pool names is &lt;code>agentgetway-oss&lt;/code>. The agent reported the name as configured rather than tidying it up, which is the correct behavior and also mildly embarrassing.)&lt;/p>
&lt;h2 id="why-the-fan-out-is-the-interesting-part">Why the fan-out is the interesting part&lt;/h2>
&lt;p>Everything above could have been a script. I want to be clear about that — &lt;code>for vip in $(list); do pool_status $vip; done&lt;/code> is not hard to write.&lt;/p>
&lt;p>What the agent added is that &lt;strong>nobody decided in advance how many calls to make.&lt;/strong> The question &amp;ldquo;why are they down&amp;rdquo; doesn&amp;rsquo;t specify a depth. Two VIPs up and 17 down produced sixteen pool lookups; a different day produces a different number. The agent read the shape of the first answer and sized the second turn to fit, then collapsed 16 identical reason strings into one finding instead of pasting sixteen JSON blobs at me.&lt;/p>
&lt;p>That&amp;rsquo;s the actual value proposition for a read-only diagnostic agent, and it&amp;rsquo;s why I&amp;rsquo;m comfortable with this one having no write tools whatsoever. The scarce skill in an outage isn&amp;rsquo;t &lt;em>changing&lt;/em> things — it&amp;rsquo;s asking the next question. This agent asks the next question and stops.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the MCP image on the k3s node before the pod starts.&lt;/li>
&lt;li>The Vault path &lt;code>secret/platform/f5-bigip&lt;/code> must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>The kagent UI at &lt;code>http://172.16.10.135:30500/&lt;/code> is &lt;strong>LAN-only&lt;/strong>.&lt;/li>
&lt;li>No generic &amp;ldquo;call any iControl path&amp;rdquo; tool, no tmsh, and &lt;strong>no write tools&lt;/strong> — the agent cannot create, delete, disable, or change virtuals, pools, or monitors.&lt;/li>
&lt;li>&lt;code>f5_system&lt;/code> returned &lt;code>null&lt;/code> product/version/build on this run. Don&amp;rsquo;t read a TMOS version into that gap; I didn&amp;rsquo;t.&lt;/li>
&lt;li>The BIG-IP is LAN-only with a self-signed certificate.&lt;/li>
&lt;li>Snapshots are in-cluster rustfs. &lt;code>gs://&lt;/code> is a prefix only.&lt;/li>
&lt;li>Never commit the F5 password or Vault secret &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Live-run record: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/f5-bigip-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / f5-bigip-sandbox-agent&lt;/a>. Manifests and the MCP image are in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> — see &lt;a href="https://github.com/sebbycorp/k8s-viper/blob/main/docs/f5-bigip-agent.md">docs/f5-bigip-agent.md&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>The other four agents in this series answer &lt;em>what is the state of things&lt;/em> — &lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS spend&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP quota&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">open tickets&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">firewall policy&lt;/a>. This one answers something different, and it&amp;rsquo;s the question I care about most on a load balancer:&lt;/p>
&lt;blockquote>
&lt;p>Which VIPs are down, and &lt;strong>why&lt;/strong>?&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;Down&amp;rdquo; is a lookup. &amp;ldquo;Why&amp;rdquo; is a fan-out — you have to walk from each offline virtual server to its pool, then to that pool&amp;rsquo;s members, and read the reason string. On my lab BIG-IP that turned into &lt;strong>18 tool calls in a single turn&lt;/strong>, and the agent did the walk itself.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-agents-grid.png" alt="kagent UI Agents grid with six SandboxAgent cards including kagent/f5-bigip" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-17. Isolated sandboxes, not plain Agents — &lt;code>kagent/f5-bigip&lt;/code> on the grid.&lt;/em>&lt;/p>
&lt;h2 id="why-a-sandboxagent-and-not-a-plain-agent">Why a SandboxAgent and not a plain Agent&lt;/h2>
&lt;p>A normal kagent &lt;code>Agent&lt;/code> is a Deployment: always on, container isolation. Fine for a cluster helper.&lt;/p>
&lt;p>This one holds credentials for the box that fronts every service in my lab. The model gets a filesystem, memory, and a network for the whole chat. Substrate puts that session in a &lt;strong>gVisor actor&lt;/strong> on WorkerPool &lt;code>kagent-default&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Isolated sandbox.&lt;/strong> gVisor&amp;rsquo;s user-space kernel sits between the model session and my Viper/k3s host. Tools call iControl REST through the MCP pod; the BIG-IP password stays in Vault, not in the actor.&lt;/li>
&lt;li>&lt;strong>Idle chats snapshot&lt;/strong> (zstd) and free the worker. The next message restores that session.&lt;/li>
&lt;li>&lt;strong>No always-on pod&lt;/strong> per conversation.&lt;/li>
&lt;li>A &lt;strong>golden snapshot&lt;/strong> you can resume.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Tradeoff on this lab:&lt;/strong> nested gVisor on dockerized k3s, and snapshots are in-cluster rustfs today (&lt;code>gs://&lt;/code> is a URI prefix only), not GCS.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="mermaid">flowchart LR
 chat[&amp;#34;Chat&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 a2a[&amp;#34;A2A sandboxes&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;ateom-gvisor:v0.0.9&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;f5-bigip-mcp&amp;#34;]
 mcp[&amp;#34;f5-bigip-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 f5[&amp;#34;BIG-IP&amp;lt;br/&amp;gt;172.16.10.10&amp;#34;]
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/f5-bigip&amp;#34;]
 eso[&amp;#34;ESO&amp;#34;]

 chat --&amp;gt; ui --&amp;gt; a2a --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; f5
 vault --&amp;gt; eso --&amp;gt; mcp

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>Vault → ESO → &lt;strong>the MCP pod&lt;/strong>. Not the actor. Same shape as the other four demos, and the reason is the same: the part of the system running the model and parsing untrusted output should never be the part holding the credential.&lt;/p>
&lt;h2 id="pins-do-not-bump">Pins (do not bump)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>kagent OSS Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.10.0-rc2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agent Substrate Helm + CRDs&lt;/td>
&lt;td>&lt;code>0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Worker image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.9&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pattern&lt;/td>
&lt;td>Go declarative &lt;code>SandboxAgent&lt;/code> + FastMCP + &lt;code>RemoteMCPServer&lt;/code> + &lt;code>ExternalSecret&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model&lt;/td>
&lt;td>&lt;code>default-model-config&lt;/code> (gpt-5.5 via agentgateway)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Box&lt;/td>
&lt;td>F5 BIG-IP · &lt;code>https://172.16.10.10&lt;/code> · LAN-only, self-signed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>rc2 always writes &lt;code>ActorTemplate&lt;/code> with &lt;code>spec.pauseImage&lt;/code> and &lt;code>env[].valueFrom.secretKeyRef&lt;/code>. Substrate &lt;strong>0.0.9&lt;/strong> accepts that shape; &lt;strong>0.0.12&lt;/strong> does not. A &lt;code>Ready=False&lt;/code> agent after a version bump is a CRD pin problem, not a reason to bump further.&lt;/p>
&lt;h2 id="build-it">Build it&lt;/h2>
&lt;p>GitOps lives in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> (&lt;code>platform/kagent-ai/f5-bigip-*.yaml&lt;/code>, &lt;code>images/f5-bigip-mcp/&lt;/code>, &lt;code>docs/f5-bigip-agent.md&lt;/code>); the demo folder is the live-run record.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>BIG-IP side.&lt;/strong> A dedicated account with a read-only role. No &lt;code>tmsh&lt;/code> shell access.&lt;/li>
&lt;li>&lt;strong>Vault.&lt;/strong> Path &lt;code>secret/platform/f5-bigip&lt;/code>, keys &lt;code>host&lt;/code>, &lt;code>username&lt;/code>, &lt;code>password&lt;/code>. ESO syncs it to the MCP pod; git holds the mapping only.&lt;/li>
&lt;li>&lt;strong>Image.&lt;/strong> Build &lt;code>f5-bigip-mcp:dev&lt;/code> and &lt;code>ctr images import&lt;/code> it onto the k3s node.&lt;/li>
&lt;li>&lt;strong>Apply&lt;/strong> the SandboxAgent, RemoteMCPServer, and ExternalSecret.&lt;/li>
&lt;/ol>
&lt;p>Live on 2026-08-17 — the whole proof surface, with nothing sensitive in it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME READY ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sandboxagent.kagent.dev/f5-bigip True True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME PROTOCOL URL ACCEPTED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">remotemcpserver.kagent.dev/f5-bigip-mcp STREAMABLE_HTTP http://f5-bigip-mcp.kagent:8084/mcp True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME STORETYPE STORE STATUS READY
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">externalsecret.../f5-bigip-mcp ClusterSecretStore vault-backend SecretSynced True
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pod/f5-bigip-mcp-7f75b47b78-mdblb 1/1 Running 0 17m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">image: f5-bigip-mcp:dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME CLASS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">actortemplate.ate.dev/f5-bigip-3adfcbf7c448a873 gvisor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">location: gs://ate-snapshots/kagent/f5-bigip
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">goldenSnapshot: gs://ate-snapshots/kagent/f5-bigip/2b9f5b6a-.../2026-08-17T14:32:42Z-23PD7HXZ5JL7OO7RNHUWZ5YOWS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">phase: Ready
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="six-tools-zero-writes">Six tools, zero writes&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>What it reads&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>f5_system&lt;/code>&lt;/td>
&lt;td>Product, version, build&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_list_vips&lt;/code>&lt;/td>
&lt;td>Virtual servers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_vip_status&lt;/code>&lt;/td>
&lt;td>One virtual server&amp;rsquo;s availability and enabled state&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_list_pools&lt;/code>&lt;/td>
&lt;td>Pools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_pool_status&lt;/code>&lt;/td>
&lt;td>One pool&amp;rsquo;s availability, reason string, and member states&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>f5_vip_brief&lt;/code>&lt;/td>
&lt;td>Composed VIP → destination → pool → availability rollup&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s it. &lt;strong>No write tools exist at all&lt;/strong> — not create, not delete, not disable, not &amp;ldquo;modify this monitor.&amp;rdquo; There is also no generic &amp;ldquo;call any iControl path&amp;rdquo; tool, which is the important one: a passthrough would let anyone with chat access reach every endpoint the account can, and read-only-on-the-account would be the only remaining control.&lt;/p>
&lt;p>This is a different call than I made for &lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow&lt;/a>, where a few narrow, reversible writes earn their place. A load balancer is the wrong place to be casual: disabling the wrong VIP is an instant outage for every service behind it, and there&amp;rsquo;s no cheap undo when the thing you broke is the path your monitoring travels over. So this agent gets to observe and nothing else.&lt;/p>
&lt;h2 id="the-live-run">The live run&lt;/h2>
&lt;p>Two questions through &lt;code>/api/a2a-sandboxes/kagent/f5-bigip&lt;/code>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-17-f5-bigip-sandboxagent/ui-chat-session.png" alt="Live f5-bigip chat: BIG-IP responded but product, version and build are null; two available VIPs listed with destinations and pools; 19 total VIPs checked with the rest offline; second answer shows 18 tool calls and the pool reason string" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-17. Two tool calls on Q1, &lt;strong>18&lt;/strong> on Q2. The 17-row down table continues below the fold.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Q1 — &amp;ldquo;What is this BIG-IP running, and which VIPs are up?&amp;rdquo;&lt;/strong> (~12.6s, tools: &lt;code>f5_system&lt;/code>, &lt;code>f5_vip_brief&lt;/code>)&lt;/p>
&lt;p>&lt;code>f5_system&lt;/code> reached &lt;code>https://172.16.10.10&lt;/code> and returned &lt;code>ok: true&lt;/code> — but the identity fields came back empty. Product &lt;code>null&lt;/code>. Version &lt;code>null&lt;/code>. Build &lt;code>null&lt;/code>.&lt;/p>
&lt;p>The agent reported exactly that. It did &lt;strong>not&lt;/strong> substitute a TMOS version from anywhere, and it didn&amp;rsquo;t decide the box was unreachable either — it separated &amp;ldquo;the API answered&amp;rdquo; from &amp;ldquo;the answer was empty,&amp;rdquo; which are genuinely different failure modes on iControl.&lt;/p>
&lt;p>Then &lt;code>f5_vip_brief&lt;/code> returned &lt;strong>19&lt;/strong> virtual servers, of which two were available:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>VIP&lt;/th>
&lt;th>Destination&lt;/th>
&lt;th>Pool&lt;/th>
&lt;th>Availability&lt;/th>
&lt;th>Enabled&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>k8s_iceman_argocd_vs&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.60:443&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/k8s_iceman_argocd_pool&lt;/code>&lt;/td>
&lt;td>&lt;code>available&lt;/code>&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>k8s_iceman_kagent_vs&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.62:8080&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/k8s_iceman_kagent_pool&lt;/code>&lt;/td>
&lt;td>&lt;code>available&lt;/code>&lt;/td>
&lt;td>&lt;code>true&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The other 17: &lt;code>offline&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Q2 — &amp;ldquo;Which VIPs are down, and why (pool members)?&amp;rdquo;&lt;/strong> (~36.4s, tools: &lt;code>f5_system&lt;/code>, &lt;code>f5_vip_brief&lt;/code>, &lt;code>f5_pool_status&lt;/code> ×16)&lt;/p>
&lt;p>This is where a single-shot tool wouldn&amp;rsquo;t have helped. The agent walked each offline VIP to its pool and pulled the status individually — sixteen &lt;code>f5_pool_status&lt;/code> calls — and every one came back the same way:&lt;/p>
&lt;ul>
&lt;li>Pool availability &lt;code>offline&lt;/code>, pool state &lt;code>enabled&lt;/code>&lt;/li>
&lt;li>Reason: &lt;strong>&amp;ldquo;The children pool member(s) are down&amp;rdquo;&lt;/strong>&lt;/li>
&lt;li>Members &lt;code>state: down&lt;/code>, &lt;code>session: monitor-enabled&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>And the conclusion that actually matters: &lt;strong>all 17 offline VIPs were still &lt;code>enabled&lt;/code>.&lt;/strong> Nobody administratively disabled anything. The VIPs are fine; the backends are gone. On my lab that&amp;rsquo;s the expected story — those pools point at Talos and k3s node ports across clusters I&amp;rsquo;d shut down — but &amp;ldquo;config is fine, backends are dead&amp;rdquo; versus &amp;ldquo;someone disabled the VIP&amp;rdquo; is the entire first branch of a load balancer triage tree, and the agent got there on its own.&lt;/p>
&lt;p>A sample of the 17, with the down members it named:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>VIP&lt;/th>
&lt;th>Destination&lt;/th>
&lt;th>Pool&lt;/th>
&lt;th>Down members&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agentgateway-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.30:8080&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/agentgetway-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.144:30344&lt;/code>, &lt;code>.144:30513&lt;/code>, &lt;code>.148:30344&lt;/code>, &lt;code>.148:30513&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>k8s_iceman_vault_vs&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.61:8200&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/k8s_iceman_vault_pool&lt;/code>&lt;/td>
&lt;td>&lt;code>talos-cp:30820&lt;/code>, &lt;code>talos-worker:30820&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.36:80&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/kagent-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.144:31438&lt;/code>, &lt;code>.144:32002&lt;/code>, &lt;code>.148:31438&lt;/code>, &lt;code>.148:32002&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vs_mcp_gateway&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.123:8090&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/pool_mcp_gateway&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.130:30168&lt;/code>, &lt;code>.132:30168&lt;/code>, &lt;code>.133:30168&lt;/code>, &lt;code>.136:30168&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>webui-https&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/172.16.20.31:443&lt;/code>&lt;/td>
&lt;td>&lt;code>/Common/webui-oss&lt;/code>&lt;/td>
&lt;td>&lt;code>172.16.10.144:30694&lt;/code>, &lt;code>172.16.10.148:30694&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Final tally: &lt;strong>2 available, 17 offline, 19 total.&lt;/strong>&lt;/p>
&lt;p>(Yes, one of my pool names is &lt;code>agentgetway-oss&lt;/code>. The agent reported the name as configured rather than tidying it up, which is the correct behavior and also mildly embarrassing.)&lt;/p>
&lt;h2 id="why-the-fan-out-is-the-interesting-part">Why the fan-out is the interesting part&lt;/h2>
&lt;p>Everything above could have been a script. I want to be clear about that — &lt;code>for vip in $(list); do pool_status $vip; done&lt;/code> is not hard to write.&lt;/p>
&lt;p>What the agent added is that &lt;strong>nobody decided in advance how many calls to make.&lt;/strong> The question &amp;ldquo;why are they down&amp;rdquo; doesn&amp;rsquo;t specify a depth. Two VIPs up and 17 down produced sixteen pool lookups; a different day produces a different number. The agent read the shape of the first answer and sized the second turn to fit, then collapsed 16 identical reason strings into one finding instead of pasting sixteen JSON blobs at me.&lt;/p>
&lt;p>That&amp;rsquo;s the actual value proposition for a read-only diagnostic agent, and it&amp;rsquo;s why I&amp;rsquo;m comfortable with this one having no write tools whatsoever. The scarce skill in an outage isn&amp;rsquo;t &lt;em>changing&lt;/em> things — it&amp;rsquo;s asking the next question. This agent asks the next question and stops.&lt;/p>
&lt;h2 id="honest-limits">Honest limits&lt;/h2>
&lt;ul>
&lt;li>Import the MCP image on the k3s node before the pod starts.&lt;/li>
&lt;li>The Vault path &lt;code>secret/platform/f5-bigip&lt;/code> must exist or the ExternalSecret stays unsynced.&lt;/li>
&lt;li>The kagent UI at &lt;code>http://172.16.10.135:30500/&lt;/code> is &lt;strong>LAN-only&lt;/strong>.&lt;/li>
&lt;li>No generic &amp;ldquo;call any iControl path&amp;rdquo; tool, no tmsh, and &lt;strong>no write tools&lt;/strong> — the agent cannot create, delete, disable, or change virtuals, pools, or monitors.&lt;/li>
&lt;li>&lt;code>f5_system&lt;/code> returned &lt;code>null&lt;/code> product/version/build on this run. Don&amp;rsquo;t read a TMOS version into that gap; I didn&amp;rsquo;t.&lt;/li>
&lt;li>The BIG-IP is LAN-only with a self-signed certificate.&lt;/li>
&lt;li>Snapshots are in-cluster rustfs. &lt;code>gs://&lt;/code> is a prefix only.&lt;/li>
&lt;li>Never commit the F5 password or Vault secret &lt;strong>values&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rest-of-the-series">The rest of the series&lt;/h2>
&lt;p>Five demos, one runtime — same pins, same Vault/ESO shape, same gVisor wall. Different blast radius each time.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-aws-budget-sandboxagent-kagent-howto/">AWS budget&lt;/a>&lt;/strong> — Cost Explorer, least-privilege IAM, and the full build from pins to golden snapshot.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-gcp-budget-sandboxagent-kagent-howto/">GCP budget&lt;/a>&lt;/strong> — us-east1 capacity, and why the billing half honestly reports &lt;em>unavailable&lt;/em>.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-servicenow-sandboxagent-kagent-howto/">ServiceNow triage&lt;/a>&lt;/strong> — 25 incidents into a manager briefing, and where write tools belong.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-17-fortigate-sandboxagent-kagent-howto/">FortiGate 80F&lt;/a>&lt;/strong> — my actual home firewall, 22 tools, and 3,007,844 hits on one policy.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/">Why secure sandbox substrates are the future&lt;/a>&lt;/strong> — the argument behind all five.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;em>Live-run record: &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/f5-bigip-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / f5-bigip-sandbox-agent&lt;/a>. Manifests and the MCP image are in &lt;a href="https://github.com/sebbycorp/k8s-viper">sebbycorp/k8s-viper&lt;/a> — see &lt;a href="https://github.com/sebbycorp/k8s-viper/blob/main/docs/f5-bigip-agent.md">docs/f5-bigip-agent.md&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>Why Secure Sandbox Substrates Are the Future of AI Agents</title><link>https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/</link><pubDate>Sun, 16 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/</guid><description>&lt;p>For a couple of years, &amp;ldquo;AI safety&amp;rdquo; in production mostly meant &lt;em>content&lt;/em> safety: don&amp;rsquo;t let the model say the wrong thing, leak a prompt, or hallucinate a number. That framing is now dangerously incomplete. Modern agents don&amp;rsquo;t just emit text — they &lt;strong>act&lt;/strong>. They get a filesystem, a chunk of memory, and a live network connection for the entire length of a conversation, and they use tools that reach into your cloud accounts, your firewalls, and your data.&lt;/p>
&lt;p>The moment an agent can &lt;em>do&lt;/em> things, the security question changes. It&amp;rsquo;s no longer &amp;ldquo;what can the model say?&amp;rdquo; It&amp;rsquo;s &lt;strong>&amp;ldquo;what can the model&amp;rsquo;s session touch — and what happens when it (or a tool, or a prompt injection) misbehaves?&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>This article argues that the answer is a &lt;strong>secure sandbox substrate&lt;/strong>: a runtime where the unit of isolation is the &lt;em>agent session itself&lt;/em>, not a long-lived pod. To keep it concrete, we&amp;rsquo;ll walk through a real one — an AWS budget assistant built on &lt;a href="https://kagent.dev">kagent&lt;/a> &lt;strong>Agent Substrate&lt;/strong>, running each executive conversation inside a &lt;strong>gVisor&lt;/strong> sandbox, that answers a genuinely useful question with real numbers:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>What&amp;rsquo;s our us-east-2 spend this month, and are we over capacity?&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" alt="Live kagent UI showing three SandboxAgent cards — aws-budget, fortigate, and hello-substrate — each running on OpenAI gpt-5.5" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three &lt;strong>SandboxAgent&lt;/strong> cards — not plain Agents. Each conversation runs in its own gVisor actor.&lt;/em>&lt;/p>
&lt;h2 id="the-problem-agents-are-processes-with-hands">The problem: agents are processes with hands&lt;/h2>
&lt;p>A traditional chatbot is a pure function — text in, text out. You can reason about its safety by reasoning about its words.&lt;/p>
&lt;p>An agent is a &lt;strong>process with hands&lt;/strong>. Give it an MCP server and it can call AWS Cost Explorer, describe your EC2 fleet, read your firewall config, or worse. To do that work it needs an execution environment: somewhere to run the model loop, hold conversation state, parse tool results, and keep working memory. That environment is real compute with real reach.&lt;/p>
&lt;p>So now you have to worry about the things you&amp;rsquo;d worry about for &lt;em>any&lt;/em> untrusted workload:&lt;/p>
&lt;ul>
&lt;li>A &lt;strong>prompt injection&lt;/strong> buried in a tool result that tries to make the agent do something you never asked for.&lt;/li>
&lt;li>A &lt;strong>compromised or buggy tool&lt;/strong> that the agent trusts.&lt;/li>
&lt;li>&lt;strong>Secret sprawl&lt;/strong> — API keys sitting in the same process as a model that&amp;rsquo;s actively being manipulated by untrusted input.&lt;/li>
&lt;li>&lt;strong>Blast radius&lt;/strong> — if the session breaks out, what else on the host does it reach?&lt;/li>
&lt;/ul>
&lt;p>The usual answer is &amp;ldquo;run the agent in a Kubernetes pod.&amp;rdquo; That helps, but it quietly makes two assumptions that don&amp;rsquo;t hold at agent scale: that a &lt;strong>container boundary&lt;/strong> is a strong enough wall against untrusted code, and that an &lt;strong>always-on pod per conversation&lt;/strong> is an acceptable way to spend compute. Both deserve a second look.&lt;/p>
&lt;h2 id="the-idea-make-the-session-the-unit-of-isolation">The idea: make the &lt;em>session&lt;/em> the unit of isolation&lt;/h2>
&lt;p>A secure sandbox substrate flips the model. Instead of &amp;ldquo;one long-running deployment that handles many conversations,&amp;rdquo; you get &amp;ldquo;one &lt;strong>strongly isolated, ephemeral sandbox per session&lt;/strong>, that can be checkpointed and restored on demand.&amp;rdquo;&lt;/p>
&lt;p>In kagent&amp;rsquo;s Agent Substrate, that sandbox is a &lt;strong>gVisor actor&lt;/strong>. gVisor is a user-space kernel: it intercepts the sandboxed workload&amp;rsquo;s syscalls and services them itself, so the agent session never talks directly to the host kernel. That&amp;rsquo;s a materially stronger wall than a stock container — exactly the wall you want between &amp;ldquo;a model being fed untrusted tool output&amp;rdquo; and &amp;ldquo;the node your cluster runs on.&amp;rdquo;&lt;/p>
&lt;p>kagent expresses this as a first-class resource: a &lt;code>SandboxAgent&lt;/code>, scheduled onto a &lt;code>WorkerPool&lt;/code>, running a gVisor worker image. The difference from a normal agent is not cosmetic — it&amp;rsquo;s a different execution contract.&lt;/p>
&lt;div class="mermaid">graph TB
 subgraph plain[&amp;#34;Plain Agent — a Deployment&amp;#34;]
 P1[&amp;#34;Always-on pod&amp;#34;]
 P2[&amp;#34;Container isolation&amp;lt;br/&amp;gt;(shared host kernel)&amp;#34;]
 P3[&amp;#34;One long-lived process&amp;lt;br/&amp;gt;serves every chat&amp;#34;]
 end
 subgraph sub[&amp;#34;SandboxAgent — a Substrate actor&amp;#34;]
 S1[&amp;#34;Ephemeral gVisor actor&amp;lt;br/&amp;gt;per session&amp;#34;]
 S2[&amp;#34;User-space kernel wall&amp;lt;br/&amp;gt;between session &amp;amp; host&amp;#34;]
 S3[&amp;#34;Idle → snapshot (zstd) → worker freed&amp;lt;br/&amp;gt;Next message → restore&amp;#34;]
 end
 style plain fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style sub fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style P1 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style P2 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style P3 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style S1 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style S2 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style S3 fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Plain kagent &lt;code>Agent&lt;/code>&lt;/th>
&lt;th>&lt;code>SandboxAgent&lt;/code> (Substrate)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Kubernetes shape&lt;/strong>&lt;/td>
&lt;td>A Deployment — always-on pod&lt;/td>
&lt;td>An actor on a &lt;code>WorkerPool&lt;/code>, booted per session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Isolation&lt;/strong>&lt;/td>
&lt;td>Container (shared host kernel)&lt;/td>
&lt;td>&lt;strong>gVisor&lt;/strong> user-space kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Idle cost&lt;/strong>&lt;/td>
&lt;td>A pod sits running per conversation&lt;/td>
&lt;td>Session snapshots and frees the worker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Resume&lt;/strong>&lt;/td>
&lt;td>N/A — it never left&lt;/td>
&lt;td>&lt;strong>Restore from snapshot&lt;/strong> — same session, new message&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Right fit for&lt;/strong>&lt;/td>
&lt;td>Trusted cluster helpers&lt;/td>
&lt;td>Sessions that touch money, secrets, or untrusted input&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The rule of thumb from the demo&amp;rsquo;s own docs says it well: if you only need a Python container with &lt;code>boto3&lt;/code> and no snapshot lifecycle, a plain Deployment is fine. The instant the session &lt;strong>talks to your AWS bill&lt;/strong>, you want the wall and the lifecycle.&lt;/p>
&lt;h2 id="the-demo-an-aws-budget-agent-that-cant-go-rogue">The demo: an AWS budget agent that can&amp;rsquo;t go rogue&lt;/h2>
&lt;p>The &lt;code>aws-budget&lt;/code> SandboxAgent is deliberately mundane in what it &lt;em>does&lt;/em> and deliberately strict in what it &lt;em>can&lt;/em> do. It answers executive finance-and-capacity questions for a single region (&lt;code>us-east-2&lt;/code>) — and that&amp;rsquo;s the entire point. A boring capability, locked down properly, is exactly the shape most real enterprise agents should take.&lt;/p>
&lt;p>Here it is answering the question live, with &lt;strong>10 of 10 tool calls&lt;/strong> succeeding and every number sourced from a real AWS API:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" alt="Live kagent chat: the aws-budget agent reports us-east-2 month-to-date spend of $0.67 for account 616973157416, a budget of $4.13 of $100 used, and zero EC2/ASG/RDS/EBS capacity — with a full status table and an honest capacity readout" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. MTD &lt;strong>$0.67&lt;/strong>, budget &lt;strong>$4.13 / $100&lt;/strong> (4.13% used), &lt;strong>0&lt;/strong> running EC2 / ASG / RDS / EBS. The agent even reports &amp;ldquo;Cost Explorer rightsizing API denied; Compute Optimizer not enrolled&amp;rdquo; instead of inventing a recommendation.&lt;/em>&lt;/p>
&lt;p>Notice what the agent does when a permission is missing: it says the API was &lt;strong>denied&lt;/strong>. It does not paper over the gap with a plausible-sounding guess. That honesty is a security property too — an agent that fabricates a &amp;ldquo;$0 spend&amp;rdquo; to be helpful is an agent you can&amp;rsquo;t trust with a budget.&lt;/p>
&lt;p>The tools themselves are a curated, read-mostly catalog — eleven named tools, all Describe/Get/View/List. There is &lt;strong>no generic &amp;ldquo;run any AWS CLI&amp;rdquo; tool&lt;/strong>, the single most important design decision in the whole demo:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>AWS API (typical)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>aws_whoami&lt;/code>&lt;/td>
&lt;td>&lt;code>sts:GetCallerIdentity&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_month&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_by_service&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code> grouped by service&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_budgets&lt;/code>&lt;/td>
&lt;td>&lt;code>budgets:ViewBudget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_ec2_capacity&lt;/code>&lt;/td>
&lt;td>&lt;code>ec2:DescribeInstances&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_service_quotas&lt;/code>&lt;/td>
&lt;td>&lt;code>servicequotas:GetServiceQuota&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_executive_brief&lt;/code>&lt;/td>
&lt;td>composes the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is no &lt;code>iam:Create*&lt;/code>, no &lt;code>ec2:Terminate*&lt;/code>, no &lt;code>budgets:Delete*&lt;/code>, no &lt;code>s3:*&lt;/code> on your data. The capability boundary is enforced in three independent places — the tool code, the IAM policy, and the sandbox — so a jailbreak of any one layer still hits the next.&lt;/p>
&lt;h2 id="the-anatomy-where-the-trust-boundaries-are">The anatomy: where the trust boundaries are&lt;/h2>
&lt;p>Here&amp;rsquo;s the full request path. Read it as a sequence of trust boundaries, because that&amp;rsquo;s what it is.&lt;/p>
&lt;div class="mermaid">flowchart LR
 exec[&amp;#34;Executive&amp;lt;br/&amp;gt;browser&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 sa[&amp;#34;SandboxAgent&amp;lt;br/&amp;gt;aws-budget&amp;#34;]
 pool[&amp;#34;WorkerPool&amp;lt;br/&amp;gt;kagent-default&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;(the session)&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;aws-budget-mcp&amp;#34;]
 mcp[&amp;#34;aws-budget-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 aws[&amp;#34;AWS APIs&amp;lt;br/&amp;gt;us-east-2&amp;#34;]

 exec --&amp;gt; ui --&amp;gt; sa --&amp;gt; pool --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; aws

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The critical detail is &lt;strong>where the AWS keys live&lt;/strong> — and where they don&amp;rsquo;t:&lt;/p>
&lt;div class="mermaid">flowchart TB
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/aws-budget&amp;#34;]
 eso[&amp;#34;External Secrets Operator&amp;#34;]
 mcp[&amp;#34;aws-budget-mcp pod&amp;lt;br/&amp;gt;non-root · read-only rootfs&amp;lt;br/&amp;gt;dropped caps · no SA token&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;(the model session)&amp;#34;]
 aws[&amp;#34;AWS us-east-2&amp;#34;]

 vault --&amp;gt; eso --&amp;gt; mcp
 mcp --&amp;gt;|&amp;#34;STS / Cost Explorer / EC2&amp;#34;| aws
 actor -.-&amp;gt;|&amp;#34;calls tools over MCP&amp;lt;br/&amp;gt;never sees the keys&amp;#34;| mcp

 style actor fill:#FFF7D6,stroke:#17181C,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style vault fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Trace the secret. The AWS access key lives in &lt;strong>Vault&lt;/strong>. External Secrets Operator syncs it into a Kubernetes Secret consumed only by the &lt;strong>MCP pod&lt;/strong> — which is non-root, has a read-only root filesystem, drops Linux capabilities, and doesn&amp;rsquo;t even mount a service-account token. The &lt;strong>gVisor actor — the part running the model and chewing on untrusted tool output — never holds the key at all.&lt;/strong> It calls a tool over MCP; the tool makes the AWS call; the credential stays on the far side of the sandbox wall.&lt;/p>
&lt;p>Layer that against the isolation and you get genuine defense in depth:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>gVisor&lt;/strong> keeps a manipulated session off the host kernel.&lt;/li>
&lt;li>&lt;strong>Vault + ESO&lt;/strong> keep the secret out of the session (and out of git — the repo has only the &lt;em>mapping&lt;/em>, never the values).&lt;/li>
&lt;li>&lt;strong>Least-privilege IAM&lt;/strong>, region-conditioned to &lt;code>us-east-2&lt;/code>, means even a leaked key can only &lt;em>read&lt;/em>, and only in one region.&lt;/li>
&lt;li>&lt;strong>Read-mostly tools&lt;/strong> mean the agent has no verb for &amp;ldquo;destroy.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>No single control is novel. Composing all four around &lt;em>the session&lt;/em> is what a substrate makes routine instead of heroic.&lt;/p>
&lt;h2 id="the-economics-snapshot-free-restore">The economics: snapshot, free, restore&lt;/h2>
&lt;p>Security is only half the argument. The other half is that &amp;ldquo;always-on pod per conversation&amp;rdquo; simply doesn&amp;rsquo;t scale to a world where every employee has a dozen agents.&lt;/p>
&lt;p>Substrate&amp;rsquo;s answer is a lifecycle borrowed from serverless, applied to stateful agent sessions:&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant U as User
 participant A as gVisor actor (session)
 participant S as Snapshot store

 U-&amp;gt;&amp;gt;A: First message — boot from golden snapshot
 A--&amp;gt;&amp;gt;U: Answer (tools run inside the sandbox)
 Note over A: Chat goes idle…
 A-&amp;gt;&amp;gt;S: Checkpoint session (zstd) → free the worker
 Note over A,S: No pod is running for this conversation now.
 U-&amp;gt;&amp;gt;A: Next message
 S-&amp;gt;&amp;gt;A: Restore the exact session state
 A--&amp;gt;&amp;gt;U: Continues as if it never left
&lt;/div>
&lt;p>When a conversation goes quiet, the actor&amp;rsquo;s memory and filesystem are &lt;strong>checkpointed (compressed with zstd)&lt;/strong> to object storage and the worker is freed. The next message &lt;strong>restores that exact session&lt;/strong> instead of cold-booting a fresh container and replaying context. You get the continuity of a long-lived process with the cost profile of something that only exists while it&amp;rsquo;s being used — plus a &lt;strong>golden snapshot&lt;/strong> you can resume deterministically.&lt;/p>
&lt;p>That&amp;rsquo;s the &amp;ldquo;substrate&amp;rdquo; in secure sandbox substrate: a fabric that boots, isolates, checkpoints, and restores agent sessions as a first-class operation.&lt;/p>
&lt;h2 id="proof-its-real-not-a-slide">Proof it&amp;rsquo;s real, not a slide&lt;/h2>
&lt;p>Everything above is running, not aspirational. The control plane reports the SandboxAgent &lt;code>Ready&lt;/code>, the RemoteMCPServer &lt;code>Accepted&lt;/code>, and the MCP pod &lt;code>1/1 Running&lt;/code> — captured live, with no secrets on screen:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" alt="Terminal on Viper showing kubectl output: sandboxagent aws-budget READY True / ACCEPTED True, remotemcpserver aws-budget-mcp with STREAMABLE_HTTP protocol, and pod aws-budget-mcp 1/1 Running — annotated &amp;amp;rsquo;no Vault tokens, no AWS secret keys&amp;amp;rsquo;" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live capture, 2026-08-16 on Viper (k3s). &lt;code>Ready=True&lt;/code>, MCP &lt;code>Accepted&lt;/code>, pod &lt;code>1/1 Running&lt;/code> — and deliberately nothing sensitive on screen.&lt;/em>&lt;/p>
&lt;p>Here&amp;rsquo;s the same live A2A turn as a short reel — question in, ten tool calls, executive-shaped answer out:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" alt="Animated reel of the aws-budget agent handling the spend-and-capacity question end to end" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>One tell that these really are sandboxed sessions and not plain Agents: the classic &lt;code>/api/a2a/&amp;lt;ns&amp;gt;/&amp;lt;name&amp;gt;&lt;/code> endpoint &lt;strong>404s&lt;/strong> (there is no &lt;code>Agent&lt;/code> CR), and the UI talks to &lt;code>/api/a2a-sandboxes/kagent/aws-budget&lt;/code> instead. The card even wears a &lt;em>Sandbox: Agent Substrate&lt;/em> badge.&lt;/p>
&lt;h2 id="being-honest-about-the-tradeoffs">Being honest about the tradeoffs&lt;/h2>
&lt;p>Substrates are early, and the demo is refreshingly candid about it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nested gVisor is finicky.&lt;/strong> Running gVisor actors on dockerized k3s can hit &lt;code>runsc&lt;/code>/seccomp/&lt;code>/dev/kvm&lt;/code> issues. That&amp;rsquo;s a worker-environment problem — not a reason to downgrade the agent to an unsandboxed Deployment.&lt;/li>
&lt;li>&lt;strong>Snapshots are local here.&lt;/strong> In this lab they land on in-cluster &lt;code>rustfs&lt;/code>; the &lt;code>gs://&lt;/code> you see is a URI &lt;em>prefix&lt;/em>, not live Google Cloud storage. Cluster-wide object storage is future work.&lt;/li>
&lt;li>&lt;strong>Version pinning matters.&lt;/strong> The demo pins kagent &lt;code>0.10.0-rc2&lt;/code> with Substrate &lt;code>0.0.9&lt;/code> on purpose — a mismatched CRD pairing (e.g. &lt;code>0.0.12&lt;/code>) makes the agent go &lt;code>Ready=False&lt;/code>. Substrates are moving fast, so pin deliberately.&lt;/li>
&lt;/ul>
&lt;p>None of these undercut the thesis. They&amp;rsquo;re the normal roughness of a capability that&amp;rsquo;s ahead of its tooling — the same place containers were a decade ago.&lt;/p>
&lt;h2 id="why-this-is-the-future">Why this is the future&lt;/h2>
&lt;p>Zoom out. Three trends are converging:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Agents are proliferating.&lt;/strong> Not one assistant per company — dozens per team, each with tools that reach real systems.&lt;/li>
&lt;li>&lt;strong>Tools mean reach.&lt;/strong> Every MCP server an agent gains is new blast radius, and much of what an agent reads (tool output, fetched pages, tickets) is untrusted input that can carry an injection.&lt;/li>
&lt;li>&lt;strong>Sessions are stateful and bursty.&lt;/strong> People talk to agents in spikes, then walk away. Paying for an always-on pod per idle conversation is indefensible at scale.&lt;/li>
&lt;/ol>
&lt;p>A secure sandbox substrate is the architecture that answers all three at once. It makes &lt;strong>strong per-session isolation&lt;/strong> the default instead of a special project. It makes &lt;strong>secrets-outside-the-session&lt;/strong> the natural shape rather than a retrofit. And it makes &lt;strong>ephemeral, resumable sessions&lt;/strong> an economic reality through snapshot and restore.&lt;/p>
&lt;p>The &lt;code>aws-budget&lt;/code> agent is a small, honest proof of the pattern: a genuinely useful assistant that reads your cloud bill, holds no keys, has no verb for destruction, runs behind a user-space kernel wall, and disappears when the conversation ends — ready to resume the instant you come back.&lt;/p>
&lt;p>That combination — &lt;strong>useful, contained, ephemeral&lt;/strong> — is what production AI agents will need to be. The substrate is how you get there without hand-building the isolation for every single one.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The full demo — manifests, the FastMCP server, IAM policy, runbooks, and the live report this article draws on — lives in &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/aws-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / aws-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;p>For a couple of years, &amp;ldquo;AI safety&amp;rdquo; in production mostly meant &lt;em>content&lt;/em> safety: don&amp;rsquo;t let the model say the wrong thing, leak a prompt, or hallucinate a number. That framing is now dangerously incomplete. Modern agents don&amp;rsquo;t just emit text — they &lt;strong>act&lt;/strong>. They get a filesystem, a chunk of memory, and a live network connection for the entire length of a conversation, and they use tools that reach into your cloud accounts, your firewalls, and your data.&lt;/p>
&lt;p>The moment an agent can &lt;em>do&lt;/em> things, the security question changes. It&amp;rsquo;s no longer &amp;ldquo;what can the model say?&amp;rdquo; It&amp;rsquo;s &lt;strong>&amp;ldquo;what can the model&amp;rsquo;s session touch — and what happens when it (or a tool, or a prompt injection) misbehaves?&amp;rdquo;&lt;/strong>&lt;/p>
&lt;p>This article argues that the answer is a &lt;strong>secure sandbox substrate&lt;/strong>: a runtime where the unit of isolation is the &lt;em>agent session itself&lt;/em>, not a long-lived pod. To keep it concrete, we&amp;rsquo;ll walk through a real one — an AWS budget assistant built on &lt;a href="https://kagent.dev">kagent&lt;/a> &lt;strong>Agent Substrate&lt;/strong>, running each executive conversation inside a &lt;strong>gVisor&lt;/strong> sandbox, that answers a genuinely useful question with real numbers:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>What&amp;rsquo;s our us-east-2 spend this month, and are we over capacity?&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-agents-grid.png" alt="Live kagent UI showing three SandboxAgent cards — aws-budget, fortigate, and hello-substrate — each running on OpenAI gpt-5.5" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. Three &lt;strong>SandboxAgent&lt;/strong> cards — not plain Agents. Each conversation runs in its own gVisor actor.&lt;/em>&lt;/p>
&lt;h2 id="the-problem-agents-are-processes-with-hands">The problem: agents are processes with hands&lt;/h2>
&lt;p>A traditional chatbot is a pure function — text in, text out. You can reason about its safety by reasoning about its words.&lt;/p>
&lt;p>An agent is a &lt;strong>process with hands&lt;/strong>. Give it an MCP server and it can call AWS Cost Explorer, describe your EC2 fleet, read your firewall config, or worse. To do that work it needs an execution environment: somewhere to run the model loop, hold conversation state, parse tool results, and keep working memory. That environment is real compute with real reach.&lt;/p>
&lt;p>So now you have to worry about the things you&amp;rsquo;d worry about for &lt;em>any&lt;/em> untrusted workload:&lt;/p>
&lt;ul>
&lt;li>A &lt;strong>prompt injection&lt;/strong> buried in a tool result that tries to make the agent do something you never asked for.&lt;/li>
&lt;li>A &lt;strong>compromised or buggy tool&lt;/strong> that the agent trusts.&lt;/li>
&lt;li>&lt;strong>Secret sprawl&lt;/strong> — API keys sitting in the same process as a model that&amp;rsquo;s actively being manipulated by untrusted input.&lt;/li>
&lt;li>&lt;strong>Blast radius&lt;/strong> — if the session breaks out, what else on the host does it reach?&lt;/li>
&lt;/ul>
&lt;p>The usual answer is &amp;ldquo;run the agent in a Kubernetes pod.&amp;rdquo; That helps, but it quietly makes two assumptions that don&amp;rsquo;t hold at agent scale: that a &lt;strong>container boundary&lt;/strong> is a strong enough wall against untrusted code, and that an &lt;strong>always-on pod per conversation&lt;/strong> is an acceptable way to spend compute. Both deserve a second look.&lt;/p>
&lt;h2 id="the-idea-make-the-session-the-unit-of-isolation">The idea: make the &lt;em>session&lt;/em> the unit of isolation&lt;/h2>
&lt;p>A secure sandbox substrate flips the model. Instead of &amp;ldquo;one long-running deployment that handles many conversations,&amp;rdquo; you get &amp;ldquo;one &lt;strong>strongly isolated, ephemeral sandbox per session&lt;/strong>, that can be checkpointed and restored on demand.&amp;rdquo;&lt;/p>
&lt;p>In kagent&amp;rsquo;s Agent Substrate, that sandbox is a &lt;strong>gVisor actor&lt;/strong>. gVisor is a user-space kernel: it intercepts the sandboxed workload&amp;rsquo;s syscalls and services them itself, so the agent session never talks directly to the host kernel. That&amp;rsquo;s a materially stronger wall than a stock container — exactly the wall you want between &amp;ldquo;a model being fed untrusted tool output&amp;rdquo; and &amp;ldquo;the node your cluster runs on.&amp;rdquo;&lt;/p>
&lt;p>kagent expresses this as a first-class resource: a &lt;code>SandboxAgent&lt;/code>, scheduled onto a &lt;code>WorkerPool&lt;/code>, running a gVisor worker image. The difference from a normal agent is not cosmetic — it&amp;rsquo;s a different execution contract.&lt;/p>
&lt;div class="mermaid">graph TB
 subgraph plain[&amp;#34;Plain Agent — a Deployment&amp;#34;]
 P1[&amp;#34;Always-on pod&amp;#34;]
 P2[&amp;#34;Container isolation&amp;lt;br/&amp;gt;(shared host kernel)&amp;#34;]
 P3[&amp;#34;One long-lived process&amp;lt;br/&amp;gt;serves every chat&amp;#34;]
 end
 subgraph sub[&amp;#34;SandboxAgent — a Substrate actor&amp;#34;]
 S1[&amp;#34;Ephemeral gVisor actor&amp;lt;br/&amp;gt;per session&amp;#34;]
 S2[&amp;#34;User-space kernel wall&amp;lt;br/&amp;gt;between session &amp;amp; host&amp;#34;]
 S3[&amp;#34;Idle → snapshot (zstd) → worker freed&amp;lt;br/&amp;gt;Next message → restore&amp;#34;]
 end
 style plain fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style sub fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style P1 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style P2 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style P3 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style S1 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style S2 fill:#FFFFFF,stroke:#17181C,color:#17181C
 style S3 fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Plain kagent &lt;code>Agent&lt;/code>&lt;/th>
&lt;th>&lt;code>SandboxAgent&lt;/code> (Substrate)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Kubernetes shape&lt;/strong>&lt;/td>
&lt;td>A Deployment — always-on pod&lt;/td>
&lt;td>An actor on a &lt;code>WorkerPool&lt;/code>, booted per session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Isolation&lt;/strong>&lt;/td>
&lt;td>Container (shared host kernel)&lt;/td>
&lt;td>&lt;strong>gVisor&lt;/strong> user-space kernel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Idle cost&lt;/strong>&lt;/td>
&lt;td>A pod sits running per conversation&lt;/td>
&lt;td>Session snapshots and frees the worker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Resume&lt;/strong>&lt;/td>
&lt;td>N/A — it never left&lt;/td>
&lt;td>&lt;strong>Restore from snapshot&lt;/strong> — same session, new message&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Right fit for&lt;/strong>&lt;/td>
&lt;td>Trusted cluster helpers&lt;/td>
&lt;td>Sessions that touch money, secrets, or untrusted input&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The rule of thumb from the demo&amp;rsquo;s own docs says it well: if you only need a Python container with &lt;code>boto3&lt;/code> and no snapshot lifecycle, a plain Deployment is fine. The instant the session &lt;strong>talks to your AWS bill&lt;/strong>, you want the wall and the lifecycle.&lt;/p>
&lt;h2 id="the-demo-an-aws-budget-agent-that-cant-go-rogue">The demo: an AWS budget agent that can&amp;rsquo;t go rogue&lt;/h2>
&lt;p>The &lt;code>aws-budget&lt;/code> SandboxAgent is deliberately mundane in what it &lt;em>does&lt;/em> and deliberately strict in what it &lt;em>can&lt;/em> do. It answers executive finance-and-capacity questions for a single region (&lt;code>us-east-2&lt;/code>) — and that&amp;rsquo;s the entire point. A boring capability, locked down properly, is exactly the shape most real enterprise agents should take.&lt;/p>
&lt;p>Here it is answering the question live, with &lt;strong>10 of 10 tool calls&lt;/strong> succeeding and every number sourced from a real AWS API:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/ui-chat-session.png" alt="Live kagent chat: the aws-budget agent reports us-east-2 month-to-date spend of $0.67 for account 616973157416, a budget of $4.13 of $100 used, and zero EC2/ASG/RDS/EBS capacity — with a full status table and an honest capacity readout" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live kagent UI, 2026-08-16. MTD &lt;strong>$0.67&lt;/strong>, budget &lt;strong>$4.13 / $100&lt;/strong> (4.13% used), &lt;strong>0&lt;/strong> running EC2 / ASG / RDS / EBS. The agent even reports &amp;ldquo;Cost Explorer rightsizing API denied; Compute Optimizer not enrolled&amp;rdquo; instead of inventing a recommendation.&lt;/em>&lt;/p>
&lt;p>Notice what the agent does when a permission is missing: it says the API was &lt;strong>denied&lt;/strong>. It does not paper over the gap with a plausible-sounding guess. That honesty is a security property too — an agent that fabricates a &amp;ldquo;$0 spend&amp;rdquo; to be helpful is an agent you can&amp;rsquo;t trust with a budget.&lt;/p>
&lt;p>The tools themselves are a curated, read-mostly catalog — eleven named tools, all Describe/Get/View/List. There is &lt;strong>no generic &amp;ldquo;run any AWS CLI&amp;rdquo; tool&lt;/strong>, the single most important design decision in the whole demo:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>AWS API (typical)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>aws_whoami&lt;/code>&lt;/td>
&lt;td>&lt;code>sts:GetCallerIdentity&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_month&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_cost_by_service&lt;/code>&lt;/td>
&lt;td>&lt;code>ce:GetCostAndUsage&lt;/code> grouped by service&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_budgets&lt;/code>&lt;/td>
&lt;td>&lt;code>budgets:ViewBudget&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_ec2_capacity&lt;/code>&lt;/td>
&lt;td>&lt;code>ec2:DescribeInstances&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_service_quotas&lt;/code>&lt;/td>
&lt;td>&lt;code>servicequotas:GetServiceQuota&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aws_executive_brief&lt;/code>&lt;/td>
&lt;td>composes the above&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is no &lt;code>iam:Create*&lt;/code>, no &lt;code>ec2:Terminate*&lt;/code>, no &lt;code>budgets:Delete*&lt;/code>, no &lt;code>s3:*&lt;/code> on your data. The capability boundary is enforced in three independent places — the tool code, the IAM policy, and the sandbox — so a jailbreak of any one layer still hits the next.&lt;/p>
&lt;h2 id="the-anatomy-where-the-trust-boundaries-are">The anatomy: where the trust boundaries are&lt;/h2>
&lt;p>Here&amp;rsquo;s the full request path. Read it as a sequence of trust boundaries, because that&amp;rsquo;s what it is.&lt;/p>
&lt;div class="mermaid">flowchart LR
 exec[&amp;#34;Executive&amp;lt;br/&amp;gt;browser&amp;#34;]
 ui[&amp;#34;kagent UI&amp;lt;br/&amp;gt;:30500&amp;#34;]
 sa[&amp;#34;SandboxAgent&amp;lt;br/&amp;gt;aws-budget&amp;#34;]
 pool[&amp;#34;WorkerPool&amp;lt;br/&amp;gt;kagent-default&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;(the session)&amp;#34;]
 rmcp[&amp;#34;RemoteMCPServer&amp;lt;br/&amp;gt;aws-budget-mcp&amp;#34;]
 mcp[&amp;#34;aws-budget-mcp&amp;lt;br/&amp;gt;:8084/mcp&amp;#34;]
 aws[&amp;#34;AWS APIs&amp;lt;br/&amp;gt;us-east-2&amp;#34;]

 exec --&amp;gt; ui --&amp;gt; sa --&amp;gt; pool --&amp;gt; actor --&amp;gt; rmcp --&amp;gt; mcp --&amp;gt; aws

 style actor fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>The critical detail is &lt;strong>where the AWS keys live&lt;/strong> — and where they don&amp;rsquo;t:&lt;/p>
&lt;div class="mermaid">flowchart TB
 vault[&amp;#34;Vault&amp;lt;br/&amp;gt;secret/platform/aws-budget&amp;#34;]
 eso[&amp;#34;External Secrets Operator&amp;#34;]
 mcp[&amp;#34;aws-budget-mcp pod&amp;lt;br/&amp;gt;non-root · read-only rootfs&amp;lt;br/&amp;gt;dropped caps · no SA token&amp;#34;]
 actor[&amp;#34;gVisor actor&amp;lt;br/&amp;gt;(the model session)&amp;#34;]
 aws[&amp;#34;AWS us-east-2&amp;#34;]

 vault --&amp;gt; eso --&amp;gt; mcp
 mcp --&amp;gt;|&amp;#34;STS / Cost Explorer / EC2&amp;#34;| aws
 actor -.-&amp;gt;|&amp;#34;calls tools over MCP&amp;lt;br/&amp;gt;never sees the keys&amp;#34;| mcp

 style actor fill:#FFF7D6,stroke:#17181C,color:#17181C
 style mcp fill:#F1EFE9,stroke:#E5341F,color:#17181C
 style vault fill:#FFFFFF,stroke:#17181C,color:#17181C
&lt;/div>
&lt;p>Trace the secret. The AWS access key lives in &lt;strong>Vault&lt;/strong>. External Secrets Operator syncs it into a Kubernetes Secret consumed only by the &lt;strong>MCP pod&lt;/strong> — which is non-root, has a read-only root filesystem, drops Linux capabilities, and doesn&amp;rsquo;t even mount a service-account token. The &lt;strong>gVisor actor — the part running the model and chewing on untrusted tool output — never holds the key at all.&lt;/strong> It calls a tool over MCP; the tool makes the AWS call; the credential stays on the far side of the sandbox wall.&lt;/p>
&lt;p>Layer that against the isolation and you get genuine defense in depth:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>gVisor&lt;/strong> keeps a manipulated session off the host kernel.&lt;/li>
&lt;li>&lt;strong>Vault + ESO&lt;/strong> keep the secret out of the session (and out of git — the repo has only the &lt;em>mapping&lt;/em>, never the values).&lt;/li>
&lt;li>&lt;strong>Least-privilege IAM&lt;/strong>, region-conditioned to &lt;code>us-east-2&lt;/code>, means even a leaked key can only &lt;em>read&lt;/em>, and only in one region.&lt;/li>
&lt;li>&lt;strong>Read-mostly tools&lt;/strong> mean the agent has no verb for &amp;ldquo;destroy.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>No single control is novel. Composing all four around &lt;em>the session&lt;/em> is what a substrate makes routine instead of heroic.&lt;/p>
&lt;h2 id="the-economics-snapshot-free-restore">The economics: snapshot, free, restore&lt;/h2>
&lt;p>Security is only half the argument. The other half is that &amp;ldquo;always-on pod per conversation&amp;rdquo; simply doesn&amp;rsquo;t scale to a world where every employee has a dozen agents.&lt;/p>
&lt;p>Substrate&amp;rsquo;s answer is a lifecycle borrowed from serverless, applied to stateful agent sessions:&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant U as User
 participant A as gVisor actor (session)
 participant S as Snapshot store

 U-&amp;gt;&amp;gt;A: First message — boot from golden snapshot
 A--&amp;gt;&amp;gt;U: Answer (tools run inside the sandbox)
 Note over A: Chat goes idle…
 A-&amp;gt;&amp;gt;S: Checkpoint session (zstd) → free the worker
 Note over A,S: No pod is running for this conversation now.
 U-&amp;gt;&amp;gt;A: Next message
 S-&amp;gt;&amp;gt;A: Restore the exact session state
 A--&amp;gt;&amp;gt;U: Continues as if it never left
&lt;/div>
&lt;p>When a conversation goes quiet, the actor&amp;rsquo;s memory and filesystem are &lt;strong>checkpointed (compressed with zstd)&lt;/strong> to object storage and the worker is freed. The next message &lt;strong>restores that exact session&lt;/strong> instead of cold-booting a fresh container and replaying context. You get the continuity of a long-lived process with the cost profile of something that only exists while it&amp;rsquo;s being used — plus a &lt;strong>golden snapshot&lt;/strong> you can resume deterministically.&lt;/p>
&lt;p>That&amp;rsquo;s the &amp;ldquo;substrate&amp;rdquo; in secure sandbox substrate: a fabric that boots, isolates, checkpoints, and restores agent sessions as a first-class operation.&lt;/p>
&lt;h2 id="proof-its-real-not-a-slide">Proof it&amp;rsquo;s real, not a slide&lt;/h2>
&lt;p>Everything above is running, not aspirational. The control plane reports the SandboxAgent &lt;code>Ready&lt;/code>, the RemoteMCPServer &lt;code>Accepted&lt;/code>, and the MCP pod &lt;code>1/1 Running&lt;/code> — captured live, with no secrets on screen:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/cli-live-status.png" alt="Terminal on Viper showing kubectl output: sandboxagent aws-budget READY True / ACCEPTED True, remotemcpserver aws-budget-mcp with STREAMABLE_HTTP protocol, and pod aws-budget-mcp 1/1 Running — annotated &amp;amp;rsquo;no Vault tokens, no AWS secret keys&amp;amp;rsquo;" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>Live capture, 2026-08-16 on Viper (k3s). &lt;code>Ready=True&lt;/code>, MCP &lt;code>Accepted&lt;/code>, pod &lt;code>1/1 Running&lt;/code> — and deliberately nothing sensitive on screen.&lt;/em>&lt;/p>
&lt;p>Here&amp;rsquo;s the same live A2A turn as a short reel — question in, ten tool calls, executive-shaped answer out:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-16-secure-sandbox-substrates-kagent-aws/aws-budget-kagent-demo.gif" alt="Animated reel of the aws-budget agent handling the spend-and-capacity question end to end" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>One tell that these really are sandboxed sessions and not plain Agents: the classic &lt;code>/api/a2a/&amp;lt;ns&amp;gt;/&amp;lt;name&amp;gt;&lt;/code> endpoint &lt;strong>404s&lt;/strong> (there is no &lt;code>Agent&lt;/code> CR), and the UI talks to &lt;code>/api/a2a-sandboxes/kagent/aws-budget&lt;/code> instead. The card even wears a &lt;em>Sandbox: Agent Substrate&lt;/em> badge.&lt;/p>
&lt;h2 id="being-honest-about-the-tradeoffs">Being honest about the tradeoffs&lt;/h2>
&lt;p>Substrates are early, and the demo is refreshingly candid about it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nested gVisor is finicky.&lt;/strong> Running gVisor actors on dockerized k3s can hit &lt;code>runsc&lt;/code>/seccomp/&lt;code>/dev/kvm&lt;/code> issues. That&amp;rsquo;s a worker-environment problem — not a reason to downgrade the agent to an unsandboxed Deployment.&lt;/li>
&lt;li>&lt;strong>Snapshots are local here.&lt;/strong> In this lab they land on in-cluster &lt;code>rustfs&lt;/code>; the &lt;code>gs://&lt;/code> you see is a URI &lt;em>prefix&lt;/em>, not live Google Cloud storage. Cluster-wide object storage is future work.&lt;/li>
&lt;li>&lt;strong>Version pinning matters.&lt;/strong> The demo pins kagent &lt;code>0.10.0-rc2&lt;/code> with Substrate &lt;code>0.0.9&lt;/code> on purpose — a mismatched CRD pairing (e.g. &lt;code>0.0.12&lt;/code>) makes the agent go &lt;code>Ready=False&lt;/code>. Substrates are moving fast, so pin deliberately.&lt;/li>
&lt;/ul>
&lt;p>None of these undercut the thesis. They&amp;rsquo;re the normal roughness of a capability that&amp;rsquo;s ahead of its tooling — the same place containers were a decade ago.&lt;/p>
&lt;h2 id="why-this-is-the-future">Why this is the future&lt;/h2>
&lt;p>Zoom out. Three trends are converging:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Agents are proliferating.&lt;/strong> Not one assistant per company — dozens per team, each with tools that reach real systems.&lt;/li>
&lt;li>&lt;strong>Tools mean reach.&lt;/strong> Every MCP server an agent gains is new blast radius, and much of what an agent reads (tool output, fetched pages, tickets) is untrusted input that can carry an injection.&lt;/li>
&lt;li>&lt;strong>Sessions are stateful and bursty.&lt;/strong> People talk to agents in spikes, then walk away. Paying for an always-on pod per idle conversation is indefensible at scale.&lt;/li>
&lt;/ol>
&lt;p>A secure sandbox substrate is the architecture that answers all three at once. It makes &lt;strong>strong per-session isolation&lt;/strong> the default instead of a special project. It makes &lt;strong>secrets-outside-the-session&lt;/strong> the natural shape rather than a retrofit. And it makes &lt;strong>ephemeral, resumable sessions&lt;/strong> an economic reality through snapshot and restore.&lt;/p>
&lt;p>The &lt;code>aws-budget&lt;/code> agent is a small, honest proof of the pattern: a genuinely useful assistant that reads your cloud bill, holds no keys, has no verb for destruction, runs behind a user-space kernel wall, and disappears when the conversation ends — ready to resume the instant you come back.&lt;/p>
&lt;p>That combination — &lt;strong>useful, contained, ephemeral&lt;/strong> — is what production AI agents will need to be. The substrate is how you get there without hand-building the isolation for every single one.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The full demo — manifests, the FastMCP server, IAM policy, runbooks, and the live report this article draws on — lives in &lt;a href="https://github.com/sebbycorp/kagent-agent-substrate-demos/tree/main/aws-sandbox-agent">sebbycorp/kagent-agent-substrate-demos / aws-sandbox-agent&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>How To: Point DeepSeek Harness at standalone agentgateway</title><link>https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/</guid><description>&lt;h2 id="first-what-is-deepseek-harness">First, what is DeepSeek Harness?&lt;/h2>
&lt;p>Two days ago — &lt;strong>August 13, 2026&lt;/strong> — DeepSeek open-sourced &lt;a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness&lt;/a>, and the README&amp;rsquo;s pitch is one line: &lt;strong>everything is a plugin.&lt;/strong>&lt;/p>
&lt;p>Not marketing-everything. Actually everything. Models, tools, skills, sessions, sandboxes, storage, the agent loop, scheduling, even the UI are all swappable from configuration. It&amp;rsquo;s MIT licensed, it&amp;rsquo;s an honest developer preview — the package reads &lt;code>0.1.0-rc.5&lt;/code> and the README promises breaking changes — and you can be running it in one command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What comes up is a local agent workspace: chat on one side, the agent&amp;rsquo;s trajectory on the other, sessions in a sidebar, a model picker at the bottom. It runs on your laptop, with filesystem and shell access, and it&amp;rsquo;s good.&lt;/p>
&lt;p>The plugin that matters for this post is the one deciding &lt;strong>where the tokens come from.&lt;/strong> Harness will talk to DeepSeek&amp;rsquo;s own models, or to any OpenAI-compatible endpoint you point it at. It asks for exactly one thing in return, on the &lt;strong>Settings → Models&lt;/strong> screen: an API key.&lt;/p>
&lt;p>That&amp;rsquo;s the moment worth pausing on. A two-day-old agent framework, running with shell access, asking for a provider key it will write to a file in my home directory — and once it has it, every call it makes is between those two parties. No one else sees the traffic. Nothing counts it. Nothing constrains it.&lt;/p>
&lt;h2 id="and-what-is-agentgateway">And what is agentgateway?&lt;/h2>
&lt;p>&lt;strong>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/strong> is an open source connectivity data plane built specifically for agent traffic — LLM calls, MCP tool calls, and agent-to-agent. I&amp;rsquo;ve written about &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">why that needs to be its own thing&lt;/a> rather than a reverse proxy with extra steps.&lt;/p>
&lt;p>For this post the relevant part is simple: it&amp;rsquo;s a single binary that speaks the OpenAI API dialect. Anything that can call OpenAI can call it instead — including Harness.&lt;/p>
&lt;h2 id="what-were-actually-here-to-do">What we&amp;rsquo;re actually here to do&lt;/h2>
&lt;p>Put the gateway between Harness and the provider, and that one hop becomes the place where five things live that the harness has no opinion about:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Govern&lt;/strong> — one place that decides what this client is allowed to ask for. Virtual keys, prompt guards, rate limits, spend caps.&lt;/li>
&lt;li>&lt;strong>Secure&lt;/strong> — the real &lt;code>OPENAI_API_KEY&lt;/code> stays inside the gateway process. Harness gets a token I invented, and it works just as well.&lt;/li>
&lt;li>&lt;strong>Route&lt;/strong> — Harness asks for a model by name; the gateway decides which provider and which exact model version actually serves it.&lt;/li>
&lt;li>&lt;strong>Log&lt;/strong> — every call, its status, and the model it resolved to, on one page. Without a proxy, this traffic is invisible.&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong> — tokens turned into dollars with a cost catalog, attributed per model and per user.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ll build it in that order, and the order matters: the first four are what make the thing work, and governance is what makes it safe to leave running. Secure, route, log, and cost come first, because you want to see traffic before you start refusing it. Then we turn govern on at the end and watch the front door get pickier.&lt;/p>
&lt;p>By the end of it I asked Harness two deliberately boring questions, and the gateway handed me a receipt: &lt;strong>39 tokens, 2 calls, $0.0000072.&lt;/strong> A meaningless amount of money, and exactly the point — that number simply does not exist when an agent talks to a provider directly.&lt;/p>
&lt;p>Here&amp;rsquo;s the whole build, including the two places I broke it.&lt;/p>
&lt;p>👉 Everything below is in the repo: &lt;strong>&lt;a href="https://github.com/sebbycorp/deepseek-agw">sebbycorp/deepseek-agw&lt;/a>&lt;/strong> · Never run the gateway binary before? Start with &lt;strong>&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">agentgateway standalone locally&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.gif" alt="DeepSeek Harness picking gpt-4o on the agentgateway provider and answering a turn" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="the-shape-of-it">The shape of it&lt;/h2>
&lt;p>Three processes on one laptop, and the only interesting part is which one holds the secret.&lt;/p>
&lt;div class="mermaid">flowchart LR
 dsh[&amp;#34;dsh web :3080&amp;#34;] --&amp;gt;|&amp;#34;dummy token /v1&amp;#34;| agw[&amp;#34;agentgateway :4002&amp;#34;]
 agw --&amp;gt;|&amp;#34;real OPENAI_API_KEY&amp;#34;| openai[OpenAI]
 agw --&amp;gt; admin[&amp;#34;admin UI :14010&amp;#34;]
 agw --&amp;gt; jaeger[&amp;#34;Jaeger :16686&amp;#34;]
&lt;/div>
&lt;p>Harness thinks it&amp;rsquo;s talking to OpenAI. It isn&amp;rsquo;t — it&amp;rsquo;s talking to a gateway on &lt;code>127.0.0.1:4002&lt;/code> that speaks the same OpenAI-compatible dialect, accepts my made-up token, and quietly swaps in the real key on the way out. Because every request passes through that one process, it&amp;rsquo;s also the natural place to count tokens, add up dollars, and emit traces.&lt;/p>
&lt;p>The rule I care about: &lt;strong>the key is not in GitHub, not in Harness&amp;rsquo;s config directory, and not in the Harness process.&lt;/strong> It lives in one file on disk with mode 600, and it gets loaded into exactly one process.&lt;/p>
&lt;hr>
&lt;h2 id="standing-up-the-gateway">Standing up the gateway&lt;/h2>
&lt;p>Node first, and this one is worth stating plainly because it cost me a confusing minute: current &lt;code>dsh&lt;/code> does not run on Node 20. Use 24.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">nvm install &lt;span class="m">24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nvm use &lt;span class="m">24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">node -v
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then the gateway itself, pinned to 1.4.1 — I&amp;rsquo;m on the &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">1.4 OSS line&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash -s -- --version v1.4.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, the piece people skip. The gateway can count tokens on its own, but tokens aren&amp;rsquo;t money. Import the cost catalog once and it can turn those counts into dollars:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p costs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agctl costs import --source models.dev --providers openai --out ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want traces too, Jaeger all-in-one is one command. Skip it if you don&amp;rsquo;t care — nothing else depends on it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name jaeger &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">COLLECTOR_OTLP_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 16686:16686 -p 4317:4317 -p 4318:4318 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> jaegertracing/all-in-one:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now the config. The thing to notice is what &lt;em>isn&amp;rsquo;t&lt;/em> in it — there&amp;rsquo;s no secret here, just a placeholder, which is why this file is safe to commit:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost:14010&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">statsAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;[::]:14030&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./costs/catalog.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://localhost:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateways&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4002&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The real key goes in its own file, locked down, and git never sees it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p .secrets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">umask&lt;/span> &lt;span class="m">077&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;export OPENAI_API_KEY=sk-...\n&amp;#39;&lt;/span> &amp;gt; .secrets/openai.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> .secrets/openai.env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And a small start script does the only clever thing in this whole setup — it sources that file into &lt;em>this process and nothing else&lt;/em>, then refuses to start if the key didn&amp;rsquo;t make it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SECRET&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">AGW_SECRET_FILE&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="p">/.secrets/openai.env&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;missing &lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OPENAI_API_KEY is empty&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> agentgateway -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="s2">/agentgateway.yaml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./start-agw.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;code>http://127.0.0.1:14010/ui&lt;/code> and make sure the admin UI is actually there before moving on. If the gateway isn&amp;rsquo;t running, everything in the next section fails in ways that look like Harness problems but aren&amp;rsquo;t.&lt;/p>
&lt;hr>
&lt;h2 id="handing-harness-a-fake-key">Handing Harness a fake key&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>local-harness-not-openai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read that token again: &lt;code>local-harness-not-openai&lt;/code>. It isn&amp;rsquo;t a credential, it&amp;rsquo;s a label. The gateway is sitting on loopback and will accept it happily, then attach the real key on the way upstream.&lt;/p>
&lt;p>Harness comes up on &lt;code>http://127.0.0.1:3080&lt;/code>. &lt;strong>Don&amp;rsquo;t send a message yet&lt;/strong> — mine failed, and I&amp;rsquo;ll get to why in a minute. Wire the provider first.&lt;/p>
&lt;p>Go to &lt;strong>Settings → Models&lt;/strong>. DeepSeek shows up red here because there&amp;rsquo;s no DeepSeek key on this laptop and I don&amp;rsquo;t want one. The custom agentgateway row is the green one:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings.png" alt="Settings → Models with DeepSeek red and the agentgateway custom provider green" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Add a custom provider and fill in the form. This is the entire integration — a base URL and a fake token:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Provider ID&lt;/td>
&lt;td>&lt;code>agw&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Display name&lt;/td>
&lt;td>&lt;code>agentgateway (OpenAI via dummy token)&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API protocol&lt;/td>
&lt;td>&lt;code>openai-completions&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Base URL&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:4002/v1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>apiKeyEnv&lt;/code>&lt;/td>
&lt;td>&lt;code>GATEWAY_API_KEY&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Key value&lt;/td>
&lt;td>&lt;code>local-harness-not-openai&lt;/code> — &lt;strong>not&lt;/strong> the real OpenAI key&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings-detail.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings-detail.png" alt="Provider detail showing base URL 127.0.0.1:4002/v1, protocol openai-completions, and a dummy key already set" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Then add the models — &lt;code>gpt-4o&lt;/code> and &lt;code>gpt-4o-mini&lt;/code> are enough to start — and change &lt;strong>max output tokens to 8192&lt;/strong> while you&amp;rsquo;re in there. That number matters more than it looks, which is the first of my two mistakes:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-models-max-tokens.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-models-max-tokens.png" alt="Customized model catalog with gpt-4o max output tokens set to 8192" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Finally, &lt;strong>New Session&lt;/strong>, and pick &lt;strong>agentgateway / gpt-4o&lt;/strong>. The picker keeps the DeepSeek models in their own group above the custom provider, so once you know to look it&amp;rsquo;s hard to get wrong:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-model-picker.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-model-picker.png" alt="Model picker with gpt-4o selected under the agentgateway provider" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>None of this has to happen in the browser, by the way. The UI is just writing &lt;code>$DSH_HOME/settings.yaml&lt;/code> — usually &lt;code>~/.dsh&lt;/code> — so you can skip the clicking entirely:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">llm-pi-ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agw&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeyEnv&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GATEWAY_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-completions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">baseURL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://127.0.0.1:4002/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8192&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8192&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The token lands in a second file, &lt;code>$DSH_HOME/.credentials.yaml&lt;/code>. Worth opening once just to reassure yourself: it holds &lt;code>GATEWAY_API_KEY&lt;/code>, the fake one. &lt;code>OPENAI_API_KEY&lt;/code> never appears anywhere in that directory.&lt;/p>
&lt;hr>
&lt;h2 id="where-i-tripped">Where I tripped&lt;/h2>
&lt;p>Two failures, ten minutes, neither one the gateway&amp;rsquo;s fault. I&amp;rsquo;m writing both down because I&amp;rsquo;d have saved the evening if someone else had.&lt;/p>
&lt;p>&lt;strong>The first turn failed on a missing key.&lt;/strong> I&amp;rsquo;d wired everything up, typed a message, and got an error about a DeepSeek credential — which made no sense, because I&amp;rsquo;d just spent twenty minutes making sure this thing pointed at OpenAI. The catch: my session had been created &lt;em>before&lt;/em> I added the provider, so it was still sitting on &lt;code>deepseek-official&lt;/code>, and there&amp;rsquo;s no &lt;code>DEEPSEEK_API_KEY&lt;/code> on this box. My careful config was real; the session just wasn&amp;rsquo;t using it. &lt;strong>New Session&lt;/strong>, pick &lt;code>agw&lt;/code>, move on.&lt;/p>
&lt;p>&lt;strong>Then &lt;code>gpt-4o&lt;/code> started returning 400.&lt;/strong> This one is sneakier. Harness defaults max output tokens to &lt;strong>32768&lt;/strong>, and &lt;code>gpt-4o&lt;/code> caps completion tokens at &lt;strong>16384&lt;/strong>. Every request was asking for more headroom than the model allows, and OpenAI was rejecting it before a single token got generated. Set it to 16384 or 8192 and it clears up immediately.&lt;/p>
&lt;p>That&amp;rsquo;s the entire troubleshooting section. Fix the provider, fix the ceiling, and it just works.&lt;/p>
&lt;hr>
&lt;h2 id="two-boring-questions">Two boring questions&lt;/h2>
&lt;p>I kept the test deliberately dull, because I wasn&amp;rsquo;t testing the model — I was testing the plumbing:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;What is 2+2? Reply with just the number.&amp;rdquo;&lt;/em> → &lt;strong>4&lt;/strong>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Name the capital of France in one word.&amp;rdquo;&lt;/em> → &lt;strong>Paris&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.png" alt="Two-question run through gpt-4o answering 4 and Paris" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Four and Paris. Not exactly a demo you&amp;rsquo;d put on a conference slide. But those two answers travelled from a UI holding a fake token, through a gateway holding the real one, out to OpenAI and back — which is the only thing I wanted to prove.&lt;/p>
&lt;p>Now the part I actually built this for. Over in the admin UI, that conversation has a receipt: &lt;strong>39 tokens, 2 calls, $0.0000072&lt;/strong>, broken out by model, user, and provider.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-ui.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-ui.png" alt="agentgateway Analytics showing 39 tokens and 2 calls in the last 24 hours" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-costs.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-costs.gif" alt="agentgateway admin UI with Analytics and the cost total for the run" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The Logs page is the proof that the dummy token really did reach OpenAI and come back: two &lt;code>CHAT&lt;/code> rows, both &lt;code>200&lt;/code>, model routing resolving &lt;code>gpt-4o-mini&lt;/code> to &lt;code>gpt-4o-mini-2024-07-18&lt;/code> on provider &lt;code>openai&lt;/code>. And no key anywhere on the page, which is the whole idea.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-logs.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-logs.png" alt="agentgateway Logs showing two CHAT 200 calls" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Scale that thought up. It&amp;rsquo;s a rounding error for two arithmetic questions, but it&amp;rsquo;s the same counter when an agent runs unattended for an hour. If you want the richer version of this view, I went deeper on it in the &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">cost and tokenomics dashboard&lt;/a> post.&lt;/p>
&lt;hr>
&lt;h2 id="now-make-the-door-pickier">Now make the door pickier&lt;/h2>
&lt;p>Everything so far gets the key out of the app and puts a number on the traffic. That&amp;rsquo;s four of the five. What&amp;rsquo;s still missing is the one that matters the moment this stops being a toy: &lt;strong>nothing yet decides who may call, how much they may spend, or what may be sent.&lt;/strong>&lt;/p>
&lt;p>Right now my gateway accepts &lt;code>local-harness-not-openai&lt;/code> because it accepts anything. That&amp;rsquo;s fine for proving a path works. It&amp;rsquo;s not fine for a framework running with shell access on a laptop that also has my SSH keys on it.&lt;/p>
&lt;p>So the config grows three policies. This is &lt;a href="https://github.com/sebbycorp/deepseek-agw/blob/main/agentgateway-governed.yaml">&lt;code>agentgateway-governed.yaml&lt;/code>&lt;/a> in the repo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># GOVERN — who is allowed through this door, and under what name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">strict&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">keys&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$DSH_VIRTUAL_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dsh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># GOVERN — a ceiling the harness cannot talk its way past&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3600s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># count the prompt before OpenAI sees it&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># SECURE — an agent that can read files shouldn&amp;#39;t be able to paste&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># a secret into a prompt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">guardrails&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;api[_-]?key[=:]\\s*\\S+&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sk-[A-Za-z0-9_-]{10,}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">builtin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">email&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three things change, and each one is worth understanding rather than pasting.&lt;/p>
&lt;p>&lt;strong>&lt;code>mode: strict&lt;/code> turns my invented token into a real one.&lt;/strong> Not real as in OpenAI — it still does nothing at &lt;code>api.openai.com&lt;/code>. Real as in &lt;em>the gateway now recognizes it&lt;/em>, and stamps &lt;code>user: dsh&lt;/code> onto every call it authorizes. That&amp;rsquo;s the difference between a cost page that says &amp;ldquo;someone spent this&amp;rdquo; and one that says who. Once you care who paid, &lt;code>optional&lt;/code> is just a hole.&lt;/p>
&lt;p>&lt;strong>The rate limit is the thing that lets me sleep.&lt;/strong> An agent loop that goes wrong doesn&amp;rsquo;t fail politely, it retries — and &lt;code>tokenize: true&lt;/code> means the gateway counts the prompt &lt;em>before&lt;/em> OpenAI does, so a runaway turn gets refused without spending anything. Worth knowing on standalone: that ceiling is gateway-wide, not per key. Per-key daily budgets need a remote rate-limit server, which is a &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">different post&lt;/a>.&lt;/p>
&lt;p>&lt;strong>The guardrails exist because of what Harness is.&lt;/strong> This is a framework with filesystem and shell access, driving a model that decides for itself what to include in a prompt. I don&amp;rsquo;t think it will paste my &lt;code>.env&lt;/code> into a completion. I&amp;rsquo;d just rather it can&amp;rsquo;t.&lt;/p>
&lt;p>Switching over is two edits. The virtual key goes in the same mode-600 file as the real one — pick any value, it&amp;rsquo;s yours:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;export DSH_VIRTUAL_KEY=sk-dsh-local-harness\n&amp;#39;&lt;/span> &amp;gt;&amp;gt; .secrets/openai.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">AGW_CONFIG&lt;/span>&lt;span class="o">=&lt;/span>./agentgateway-governed.yaml ./start-agw.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And Harness sends that instead of the placeholder — one field in &lt;strong>Settings → Models&lt;/strong>, or the environment before you start it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>sk-dsh-local-harness
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. No plugin, no patch, nothing in &lt;code>~/.dsh&lt;/code> that knows any of this happened. &lt;strong>The harness doesn&amp;rsquo;t learn about governance — it just keeps talking to &lt;code>/v1&lt;/code> and the door got pickier.&lt;/strong> That&amp;rsquo;s the whole argument for putting the control point outside the app.&lt;/p>
&lt;p>One warning from experience: if you flip to &lt;code>strict&lt;/code> and forget to update the token, every single call returns 401 and it looks exactly like the gateway is broken. And the &lt;code>email&lt;/code> builtin in that guard is more eager than you&amp;rsquo;d expect — the first time a legitimate prompt gets a 400 &lt;code>content_policy_violation&lt;/code>, that&amp;rsquo;s the rule that caught it, not a bug.&lt;/p>
&lt;hr>
&lt;h2 id="the-same-trick-on-a-cluster">The same trick on a cluster&lt;/h2>
&lt;p>Nothing about the pattern changes when this moves off the laptop. Only the hiding place for the secret does — the mode-600 file becomes a Kubernetes Secret, and the local YAML becomes a handful of CRDs. The repo has them as applyable files under &lt;a href="https://github.com/sebbycorp/deepseek-agw/tree/main/k8s">&lt;code>k8s/&lt;/code>&lt;/a>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>On my laptop&lt;/th>
&lt;th>On a cluster&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>mode-600 &lt;code>.secrets/openai.env&lt;/code>&lt;/td>
&lt;td>&lt;code>openai-secret&lt;/code> Secret, &lt;code>Authorization&lt;/code> key&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llm.models&lt;/code> in the YAML&lt;/td>
&lt;td>&lt;code>AgentgatewayBackend&lt;/code> + a &lt;code>/v1&lt;/code> &lt;code>HTTPRoute&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config.modelCatalog&lt;/code>&lt;/td>
&lt;td>ConfigMap + &lt;code>AgentgatewayParameters&lt;/code> on the &lt;strong>Gateway&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config.tracing&lt;/code>&lt;/td>
&lt;td>&lt;code>AgentgatewayPolicy&lt;/code> → Jaeger&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>http://127.0.0.1:4002/v1&lt;/code>&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:8080/v1&lt;/code> through a port-forward&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Harness doesn&amp;rsquo;t notice the difference. Same provider form, same fake token, one different URL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system deploy/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>local-harness-not-openai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One trap worth repeating, because it fails silently: the cost catalog has to be attached to the &lt;strong>Gateway&lt;/strong> via &lt;code>AgentgatewayParameters&lt;/code>. Hang it off the GatewayClass and it&amp;rsquo;s simply ignored — no error, just no dollar figures.&lt;/p>
&lt;p>&lt;strong>Being straight with you:&lt;/strong> the standalone path is the one I actually ran, and every screenshot above comes from it. The manifests mirror the 1.4.x CRDs, but I didn&amp;rsquo;t stand a cluster up for this one, so treat them as a starting point rather than something I&amp;rsquo;ve proven. For a cluster walkthrough I &lt;em>have&lt;/em> run, see &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="why-i-keep-doing-this">Why I keep doing this&lt;/h2>
&lt;p>The setup takes an evening. What I get back is the five things I opened with, and they&amp;rsquo;re all boring in the best way.&lt;/p>
&lt;p>The real key lives in one file, loaded by one process, and &lt;code>~/.dsh&lt;/code> holds a virtual key that&amp;rsquo;s worthless anywhere else — &lt;strong>secure&lt;/strong>. Rotating the real one means editing a file and restarting a single thing, instead of hunting through four config directories. Harness asks for &lt;code>gpt-4o&lt;/code> and the gateway decides what actually answers — &lt;strong>route&lt;/strong>. Every call is on a page with its status and resolved model — &lt;strong>log&lt;/strong>. When someone asks what an agent cost to run, I have a number instead of a shrug — &lt;strong>cost&lt;/strong>. And the door only opens for a key I issued, under a token ceiling, refusing prompts that look like secrets — &lt;strong>govern&lt;/strong>.&lt;/p>
&lt;p>That last one is the reason this is worth an evening rather than a &lt;code>curl&lt;/code>. The first four make the setup pleasant. Governance is what makes it something you can leave running while you&amp;rsquo;re asleep, or hand to someone who isn&amp;rsquo;t you. And notice where all of it lives: five controls, none of them inside the agent framework. Harness never learned that any of this exists.&lt;/p>
&lt;p>MCP isn&amp;rsquo;t wired into this one yet — same gateway, later. Given that Harness makes &lt;em>everything&lt;/em> a plugin, including its tools, that&amp;rsquo;s the obvious next stop. But the shape keeps repeating. Whether it&amp;rsquo;s &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code and Codex&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">OpenMausBot&lt;/a>, or DeepSeek Harness, the app stays on loopback with a fake token and the secret stays in the gateway. Every new toy just points at &lt;code>/v1&lt;/code>.&lt;/p>
&lt;p>Which means the next one takes ten minutes, not an evening. That&amp;rsquo;s really why I bother.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Repo: &lt;a href="https://github.com/sebbycorp/deepseek-agw">sebbycorp/deepseek-agw&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/">agentgateway LLM clients&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">How To: Run agentgateway standalone locally&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">How To: Point OpenMausBot at standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">How To: Connect Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">agentgateway standalone cost &amp;amp; tokenomics dashboard&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">agentgateway 1.4 OSS: what changed from 1.3&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/">Proxying all your LLM traffic through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h2 id="first-what-is-deepseek-harness">First, what is DeepSeek Harness?&lt;/h2>
&lt;p>Two days ago — &lt;strong>August 13, 2026&lt;/strong> — DeepSeek open-sourced &lt;a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness&lt;/a>, and the README&amp;rsquo;s pitch is one line: &lt;strong>everything is a plugin.&lt;/strong>&lt;/p>
&lt;p>Not marketing-everything. Actually everything. Models, tools, skills, sessions, sandboxes, storage, the agent loop, scheduling, even the UI are all swappable from configuration. It&amp;rsquo;s MIT licensed, it&amp;rsquo;s an honest developer preview — the package reads &lt;code>0.1.0-rc.5&lt;/code> and the README promises breaking changes — and you can be running it in one command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What comes up is a local agent workspace: chat on one side, the agent&amp;rsquo;s trajectory on the other, sessions in a sidebar, a model picker at the bottom. It runs on your laptop, with filesystem and shell access, and it&amp;rsquo;s good.&lt;/p>
&lt;p>The plugin that matters for this post is the one deciding &lt;strong>where the tokens come from.&lt;/strong> Harness will talk to DeepSeek&amp;rsquo;s own models, or to any OpenAI-compatible endpoint you point it at. It asks for exactly one thing in return, on the &lt;strong>Settings → Models&lt;/strong> screen: an API key.&lt;/p>
&lt;p>That&amp;rsquo;s the moment worth pausing on. A two-day-old agent framework, running with shell access, asking for a provider key it will write to a file in my home directory — and once it has it, every call it makes is between those two parties. No one else sees the traffic. Nothing counts it. Nothing constrains it.&lt;/p>
&lt;h2 id="and-what-is-agentgateway">And what is agentgateway?&lt;/h2>
&lt;p>&lt;strong>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/strong> is an open source connectivity data plane built specifically for agent traffic — LLM calls, MCP tool calls, and agent-to-agent. I&amp;rsquo;ve written about &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">why that needs to be its own thing&lt;/a> rather than a reverse proxy with extra steps.&lt;/p>
&lt;p>For this post the relevant part is simple: it&amp;rsquo;s a single binary that speaks the OpenAI API dialect. Anything that can call OpenAI can call it instead — including Harness.&lt;/p>
&lt;h2 id="what-were-actually-here-to-do">What we&amp;rsquo;re actually here to do&lt;/h2>
&lt;p>Put the gateway between Harness and the provider, and that one hop becomes the place where five things live that the harness has no opinion about:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Govern&lt;/strong> — one place that decides what this client is allowed to ask for. Virtual keys, prompt guards, rate limits, spend caps.&lt;/li>
&lt;li>&lt;strong>Secure&lt;/strong> — the real &lt;code>OPENAI_API_KEY&lt;/code> stays inside the gateway process. Harness gets a token I invented, and it works just as well.&lt;/li>
&lt;li>&lt;strong>Route&lt;/strong> — Harness asks for a model by name; the gateway decides which provider and which exact model version actually serves it.&lt;/li>
&lt;li>&lt;strong>Log&lt;/strong> — every call, its status, and the model it resolved to, on one page. Without a proxy, this traffic is invisible.&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong> — tokens turned into dollars with a cost catalog, attributed per model and per user.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ll build it in that order, and the order matters: the first four are what make the thing work, and governance is what makes it safe to leave running. Secure, route, log, and cost come first, because you want to see traffic before you start refusing it. Then we turn govern on at the end and watch the front door get pickier.&lt;/p>
&lt;p>By the end of it I asked Harness two deliberately boring questions, and the gateway handed me a receipt: &lt;strong>39 tokens, 2 calls, $0.0000072.&lt;/strong> A meaningless amount of money, and exactly the point — that number simply does not exist when an agent talks to a provider directly.&lt;/p>
&lt;p>Here&amp;rsquo;s the whole build, including the two places I broke it.&lt;/p>
&lt;p>👉 Everything below is in the repo: &lt;strong>&lt;a href="https://github.com/sebbycorp/deepseek-agw">sebbycorp/deepseek-agw&lt;/a>&lt;/strong> · Never run the gateway binary before? Start with &lt;strong>&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">agentgateway standalone locally&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.gif" alt="DeepSeek Harness picking gpt-4o on the agentgateway provider and answering a turn" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="the-shape-of-it">The shape of it&lt;/h2>
&lt;p>Three processes on one laptop, and the only interesting part is which one holds the secret.&lt;/p>
&lt;div class="mermaid">flowchart LR
 dsh[&amp;#34;dsh web :3080&amp;#34;] --&amp;gt;|&amp;#34;dummy token /v1&amp;#34;| agw[&amp;#34;agentgateway :4002&amp;#34;]
 agw --&amp;gt;|&amp;#34;real OPENAI_API_KEY&amp;#34;| openai[OpenAI]
 agw --&amp;gt; admin[&amp;#34;admin UI :14010&amp;#34;]
 agw --&amp;gt; jaeger[&amp;#34;Jaeger :16686&amp;#34;]
&lt;/div>
&lt;p>Harness thinks it&amp;rsquo;s talking to OpenAI. It isn&amp;rsquo;t — it&amp;rsquo;s talking to a gateway on &lt;code>127.0.0.1:4002&lt;/code> that speaks the same OpenAI-compatible dialect, accepts my made-up token, and quietly swaps in the real key on the way out. Because every request passes through that one process, it&amp;rsquo;s also the natural place to count tokens, add up dollars, and emit traces.&lt;/p>
&lt;p>The rule I care about: &lt;strong>the key is not in GitHub, not in Harness&amp;rsquo;s config directory, and not in the Harness process.&lt;/strong> It lives in one file on disk with mode 600, and it gets loaded into exactly one process.&lt;/p>
&lt;hr>
&lt;h2 id="standing-up-the-gateway">Standing up the gateway&lt;/h2>
&lt;p>Node first, and this one is worth stating plainly because it cost me a confusing minute: current &lt;code>dsh&lt;/code> does not run on Node 20. Use 24.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">nvm install &lt;span class="m">24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">nvm use &lt;span class="m">24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">node -v
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then the gateway itself, pinned to 1.4.1 — I&amp;rsquo;m on the &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">1.4 OSS line&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash -s -- --version v1.4.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, the piece people skip. The gateway can count tokens on its own, but tokens aren&amp;rsquo;t money. Import the cost catalog once and it can turn those counts into dollars:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p costs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agctl costs import --source models.dev --providers openai --out ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want traces too, Jaeger all-in-one is one command. Skip it if you don&amp;rsquo;t care — nothing else depends on it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name jaeger &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">COLLECTOR_OTLP_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 16686:16686 -p 4317:4317 -p 4318:4318 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> jaegertracing/all-in-one:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now the config. The thing to notice is what &lt;em>isn&amp;rsquo;t&lt;/em> in it — there&amp;rsquo;s no secret here, just a placeholder, which is why this file is safe to commit:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost:14010&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">statsAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;[::]:14030&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./costs/catalog.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://localhost:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateways&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4002&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The real key goes in its own file, locked down, and git never sees it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p .secrets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">umask&lt;/span> &lt;span class="m">077&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;export OPENAI_API_KEY=sk-...\n&amp;#39;&lt;/span> &amp;gt; .secrets/openai.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> .secrets/openai.env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And a small start script does the only clever thing in this whole setup — it sources that file into &lt;em>this process and nothing else&lt;/em>, then refuses to start if the key didn&amp;rsquo;t make it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SECRET&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">AGW_SECRET_FILE&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="p">/.secrets/openai.env&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;missing &lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OPENAI_API_KEY is empty&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> agentgateway -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="s2">/agentgateway.yaml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./start-agw.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;code>http://127.0.0.1:14010/ui&lt;/code> and make sure the admin UI is actually there before moving on. If the gateway isn&amp;rsquo;t running, everything in the next section fails in ways that look like Harness problems but aren&amp;rsquo;t.&lt;/p>
&lt;hr>
&lt;h2 id="handing-harness-a-fake-key">Handing Harness a fake key&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>local-harness-not-openai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read that token again: &lt;code>local-harness-not-openai&lt;/code>. It isn&amp;rsquo;t a credential, it&amp;rsquo;s a label. The gateway is sitting on loopback and will accept it happily, then attach the real key on the way upstream.&lt;/p>
&lt;p>Harness comes up on &lt;code>http://127.0.0.1:3080&lt;/code>. &lt;strong>Don&amp;rsquo;t send a message yet&lt;/strong> — mine failed, and I&amp;rsquo;ll get to why in a minute. Wire the provider first.&lt;/p>
&lt;p>Go to &lt;strong>Settings → Models&lt;/strong>. DeepSeek shows up red here because there&amp;rsquo;s no DeepSeek key on this laptop and I don&amp;rsquo;t want one. The custom agentgateway row is the green one:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings.png" alt="Settings → Models with DeepSeek red and the agentgateway custom provider green" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Add a custom provider and fill in the form. This is the entire integration — a base URL and a fake token:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Provider ID&lt;/td>
&lt;td>&lt;code>agw&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Display name&lt;/td>
&lt;td>&lt;code>agentgateway (OpenAI via dummy token)&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API protocol&lt;/td>
&lt;td>&lt;code>openai-completions&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Base URL&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:4002/v1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>apiKeyEnv&lt;/code>&lt;/td>
&lt;td>&lt;code>GATEWAY_API_KEY&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Key value&lt;/td>
&lt;td>&lt;code>local-harness-not-openai&lt;/code> — &lt;strong>not&lt;/strong> the real OpenAI key&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings-detail.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-settings-detail.png" alt="Provider detail showing base URL 127.0.0.1:4002/v1, protocol openai-completions, and a dummy key already set" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Then add the models — &lt;code>gpt-4o&lt;/code> and &lt;code>gpt-4o-mini&lt;/code> are enough to start — and change &lt;strong>max output tokens to 8192&lt;/strong> while you&amp;rsquo;re in there. That number matters more than it looks, which is the first of my two mistakes:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-models-max-tokens.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-models-max-tokens.png" alt="Customized model catalog with gpt-4o max output tokens set to 8192" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Finally, &lt;strong>New Session&lt;/strong>, and pick &lt;strong>agentgateway / gpt-4o&lt;/strong>. The picker keeps the DeepSeek models in their own group above the custom provider, so once you know to look it&amp;rsquo;s hard to get wrong:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-model-picker.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-model-picker.png" alt="Model picker with gpt-4o selected under the agentgateway provider" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>None of this has to happen in the browser, by the way. The UI is just writing &lt;code>$DSH_HOME/settings.yaml&lt;/code> — usually &lt;code>~/.dsh&lt;/code> — so you can skip the clicking entirely:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">llm-pi-ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agw&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeyEnv&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GATEWAY_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-completions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">baseURL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://127.0.0.1:4002/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8192&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8192&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The token lands in a second file, &lt;code>$DSH_HOME/.credentials.yaml&lt;/code>. Worth opening once just to reassure yourself: it holds &lt;code>GATEWAY_API_KEY&lt;/code>, the fake one. &lt;code>OPENAI_API_KEY&lt;/code> never appears anywhere in that directory.&lt;/p>
&lt;hr>
&lt;h2 id="where-i-tripped">Where I tripped&lt;/h2>
&lt;p>Two failures, ten minutes, neither one the gateway&amp;rsquo;s fault. I&amp;rsquo;m writing both down because I&amp;rsquo;d have saved the evening if someone else had.&lt;/p>
&lt;p>&lt;strong>The first turn failed on a missing key.&lt;/strong> I&amp;rsquo;d wired everything up, typed a message, and got an error about a DeepSeek credential — which made no sense, because I&amp;rsquo;d just spent twenty minutes making sure this thing pointed at OpenAI. The catch: my session had been created &lt;em>before&lt;/em> I added the provider, so it was still sitting on &lt;code>deepseek-official&lt;/code>, and there&amp;rsquo;s no &lt;code>DEEPSEEK_API_KEY&lt;/code> on this box. My careful config was real; the session just wasn&amp;rsquo;t using it. &lt;strong>New Session&lt;/strong>, pick &lt;code>agw&lt;/code>, move on.&lt;/p>
&lt;p>&lt;strong>Then &lt;code>gpt-4o&lt;/code> started returning 400.&lt;/strong> This one is sneakier. Harness defaults max output tokens to &lt;strong>32768&lt;/strong>, and &lt;code>gpt-4o&lt;/code> caps completion tokens at &lt;strong>16384&lt;/strong>. Every request was asking for more headroom than the model allows, and OpenAI was rejecting it before a single token got generated. Set it to 16384 or 8192 and it clears up immediately.&lt;/p>
&lt;p>That&amp;rsquo;s the entire troubleshooting section. Fix the provider, fix the ceiling, and it just works.&lt;/p>
&lt;hr>
&lt;h2 id="two-boring-questions">Two boring questions&lt;/h2>
&lt;p>I kept the test deliberately dull, because I wasn&amp;rsquo;t testing the model — I was testing the plumbing:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;What is 2+2? Reply with just the number.&amp;rdquo;&lt;/em> → &lt;strong>4&lt;/strong>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Name the capital of France in one word.&amp;rdquo;&lt;/em> → &lt;strong>Paris&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/harness-run.png" alt="Two-question run through gpt-4o answering 4 and Paris" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Four and Paris. Not exactly a demo you&amp;rsquo;d put on a conference slide. But those two answers travelled from a UI holding a fake token, through a gateway holding the real one, out to OpenAI and back — which is the only thing I wanted to prove.&lt;/p>
&lt;p>Now the part I actually built this for. Over in the admin UI, that conversation has a receipt: &lt;strong>39 tokens, 2 calls, $0.0000072&lt;/strong>, broken out by model, user, and provider.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-ui.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-ui.png" alt="agentgateway Analytics showing 39 tokens and 2 calls in the last 24 hours" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-costs.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-costs.gif" alt="agentgateway admin UI with Analytics and the cost total for the run" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The Logs page is the proof that the dummy token really did reach OpenAI and come back: two &lt;code>CHAT&lt;/code> rows, both &lt;code>200&lt;/code>, model routing resolving &lt;code>gpt-4o-mini&lt;/code> to &lt;code>gpt-4o-mini-2024-07-18&lt;/code> on provider &lt;code>openai&lt;/code>. And no key anywhere on the page, which is the whole idea.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-logs.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-deepseek-harness-agentgateway/agw-logs.png" alt="agentgateway Logs showing two CHAT 200 calls" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Scale that thought up. It&amp;rsquo;s a rounding error for two arithmetic questions, but it&amp;rsquo;s the same counter when an agent runs unattended for an hour. If you want the richer version of this view, I went deeper on it in the &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">cost and tokenomics dashboard&lt;/a> post.&lt;/p>
&lt;hr>
&lt;h2 id="now-make-the-door-pickier">Now make the door pickier&lt;/h2>
&lt;p>Everything so far gets the key out of the app and puts a number on the traffic. That&amp;rsquo;s four of the five. What&amp;rsquo;s still missing is the one that matters the moment this stops being a toy: &lt;strong>nothing yet decides who may call, how much they may spend, or what may be sent.&lt;/strong>&lt;/p>
&lt;p>Right now my gateway accepts &lt;code>local-harness-not-openai&lt;/code> because it accepts anything. That&amp;rsquo;s fine for proving a path works. It&amp;rsquo;s not fine for a framework running with shell access on a laptop that also has my SSH keys on it.&lt;/p>
&lt;p>So the config grows three policies. This is &lt;a href="https://github.com/sebbycorp/deepseek-agw/blob/main/agentgateway-governed.yaml">&lt;code>agentgateway-governed.yaml&lt;/code>&lt;/a> in the repo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># GOVERN — who is allowed through this door, and under what name&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">strict&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">keys&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$DSH_VIRTUAL_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dsh&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># GOVERN — a ceiling the harness cannot talk its way past&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3600s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># count the prompt before OpenAI sees it&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># SECURE — an agent that can read files shouldn&amp;#39;t be able to paste&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># a secret into a prompt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">guardrails&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;api[_-]?key[=:]\\s*\\S+&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sk-[A-Za-z0-9_-]{10,}&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">builtin&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">email&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three things change, and each one is worth understanding rather than pasting.&lt;/p>
&lt;p>&lt;strong>&lt;code>mode: strict&lt;/code> turns my invented token into a real one.&lt;/strong> Not real as in OpenAI — it still does nothing at &lt;code>api.openai.com&lt;/code>. Real as in &lt;em>the gateway now recognizes it&lt;/em>, and stamps &lt;code>user: dsh&lt;/code> onto every call it authorizes. That&amp;rsquo;s the difference between a cost page that says &amp;ldquo;someone spent this&amp;rdquo; and one that says who. Once you care who paid, &lt;code>optional&lt;/code> is just a hole.&lt;/p>
&lt;p>&lt;strong>The rate limit is the thing that lets me sleep.&lt;/strong> An agent loop that goes wrong doesn&amp;rsquo;t fail politely, it retries — and &lt;code>tokenize: true&lt;/code> means the gateway counts the prompt &lt;em>before&lt;/em> OpenAI does, so a runaway turn gets refused without spending anything. Worth knowing on standalone: that ceiling is gateway-wide, not per key. Per-key daily budgets need a remote rate-limit server, which is a &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">different post&lt;/a>.&lt;/p>
&lt;p>&lt;strong>The guardrails exist because of what Harness is.&lt;/strong> This is a framework with filesystem and shell access, driving a model that decides for itself what to include in a prompt. I don&amp;rsquo;t think it will paste my &lt;code>.env&lt;/code> into a completion. I&amp;rsquo;d just rather it can&amp;rsquo;t.&lt;/p>
&lt;p>Switching over is two edits. The virtual key goes in the same mode-600 file as the real one — pick any value, it&amp;rsquo;s yours:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;export DSH_VIRTUAL_KEY=sk-dsh-local-harness\n&amp;#39;&lt;/span> &amp;gt;&amp;gt; .secrets/openai.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">AGW_CONFIG&lt;/span>&lt;span class="o">=&lt;/span>./agentgateway-governed.yaml ./start-agw.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And Harness sends that instead of the placeholder — one field in &lt;strong>Settings → Models&lt;/strong>, or the environment before you start it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>sk-dsh-local-harness
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. No plugin, no patch, nothing in &lt;code>~/.dsh&lt;/code> that knows any of this happened. &lt;strong>The harness doesn&amp;rsquo;t learn about governance — it just keeps talking to &lt;code>/v1&lt;/code> and the door got pickier.&lt;/strong> That&amp;rsquo;s the whole argument for putting the control point outside the app.&lt;/p>
&lt;p>One warning from experience: if you flip to &lt;code>strict&lt;/code> and forget to update the token, every single call returns 401 and it looks exactly like the gateway is broken. And the &lt;code>email&lt;/code> builtin in that guard is more eager than you&amp;rsquo;d expect — the first time a legitimate prompt gets a 400 &lt;code>content_policy_violation&lt;/code>, that&amp;rsquo;s the rule that caught it, not a bug.&lt;/p>
&lt;hr>
&lt;h2 id="the-same-trick-on-a-cluster">The same trick on a cluster&lt;/h2>
&lt;p>Nothing about the pattern changes when this moves off the laptop. Only the hiding place for the secret does — the mode-600 file becomes a Kubernetes Secret, and the local YAML becomes a handful of CRDs. The repo has them as applyable files under &lt;a href="https://github.com/sebbycorp/deepseek-agw/tree/main/k8s">&lt;code>k8s/&lt;/code>&lt;/a>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>On my laptop&lt;/th>
&lt;th>On a cluster&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>mode-600 &lt;code>.secrets/openai.env&lt;/code>&lt;/td>
&lt;td>&lt;code>openai-secret&lt;/code> Secret, &lt;code>Authorization&lt;/code> key&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>llm.models&lt;/code> in the YAML&lt;/td>
&lt;td>&lt;code>AgentgatewayBackend&lt;/code> + a &lt;code>/v1&lt;/code> &lt;code>HTTPRoute&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config.modelCatalog&lt;/code>&lt;/td>
&lt;td>ConfigMap + &lt;code>AgentgatewayParameters&lt;/code> on the &lt;strong>Gateway&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config.tracing&lt;/code>&lt;/td>
&lt;td>&lt;code>AgentgatewayPolicy&lt;/code> → Jaeger&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>http://127.0.0.1:4002/v1&lt;/code>&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:8080/v1&lt;/code> through a port-forward&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Harness doesn&amp;rsquo;t notice the difference. Same provider form, same fake token, one different URL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system deploy/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>local-harness-not-openai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">npx @deepseek-ai/dsh web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One trap worth repeating, because it fails silently: the cost catalog has to be attached to the &lt;strong>Gateway&lt;/strong> via &lt;code>AgentgatewayParameters&lt;/code>. Hang it off the GatewayClass and it&amp;rsquo;s simply ignored — no error, just no dollar figures.&lt;/p>
&lt;p>&lt;strong>Being straight with you:&lt;/strong> the standalone path is the one I actually ran, and every screenshot above comes from it. The manifests mirror the 1.4.x CRDs, but I didn&amp;rsquo;t stand a cluster up for this one, so treat them as a starting point rather than something I&amp;rsquo;ve proven. For a cluster walkthrough I &lt;em>have&lt;/em> run, see &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="why-i-keep-doing-this">Why I keep doing this&lt;/h2>
&lt;p>The setup takes an evening. What I get back is the five things I opened with, and they&amp;rsquo;re all boring in the best way.&lt;/p>
&lt;p>The real key lives in one file, loaded by one process, and &lt;code>~/.dsh&lt;/code> holds a virtual key that&amp;rsquo;s worthless anywhere else — &lt;strong>secure&lt;/strong>. Rotating the real one means editing a file and restarting a single thing, instead of hunting through four config directories. Harness asks for &lt;code>gpt-4o&lt;/code> and the gateway decides what actually answers — &lt;strong>route&lt;/strong>. Every call is on a page with its status and resolved model — &lt;strong>log&lt;/strong>. When someone asks what an agent cost to run, I have a number instead of a shrug — &lt;strong>cost&lt;/strong>. And the door only opens for a key I issued, under a token ceiling, refusing prompts that look like secrets — &lt;strong>govern&lt;/strong>.&lt;/p>
&lt;p>That last one is the reason this is worth an evening rather than a &lt;code>curl&lt;/code>. The first four make the setup pleasant. Governance is what makes it something you can leave running while you&amp;rsquo;re asleep, or hand to someone who isn&amp;rsquo;t you. And notice where all of it lives: five controls, none of them inside the agent framework. Harness never learned that any of this exists.&lt;/p>
&lt;p>MCP isn&amp;rsquo;t wired into this one yet — same gateway, later. Given that Harness makes &lt;em>everything&lt;/em> a plugin, including its tools, that&amp;rsquo;s the obvious next stop. But the shape keeps repeating. Whether it&amp;rsquo;s &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code and Codex&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">OpenMausBot&lt;/a>, or DeepSeek Harness, the app stays on loopback with a fake token and the secret stays in the gateway. Every new toy just points at &lt;code>/v1&lt;/code>.&lt;/p>
&lt;p>Which means the next one takes ten minutes, not an evening. That&amp;rsquo;s really why I bother.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Repo: &lt;a href="https://github.com/sebbycorp/deepseek-agw">sebbycorp/deepseek-agw&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/">agentgateway LLM clients&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">How To: Run agentgateway standalone locally&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">How To: Point OpenMausBot at standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">How To: Connect Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">agentgateway standalone cost &amp;amp; tokenomics dashboard&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">agentgateway 1.4 OSS: what changed from 1.3&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/">Proxying all your LLM traffic through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>How To: Point Grok Build at standalone agentgateway</title><link>https://maniak.io/articles/2026-08-15-grok-build-standalone-agentgateway/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-15-grok-build-standalone-agentgateway/</guid><description>&lt;h2 id="first-what-is-grok-build">First, what is Grok Build?&lt;/h2>
&lt;p>&lt;strong>Grok Build&lt;/strong> is xAI&amp;rsquo;s coding agent. It lives in the terminal, reads the repo, edits files, and shells out. Out of the box it talks straight to &lt;code>api.x.ai&lt;/code> with a key sitting in &lt;code>~/.grok&lt;/code> or an env var.&lt;/p>
&lt;p>That&amp;rsquo;s fine for one laptop. It&amp;rsquo;s ugly the moment you have more than one tool, more than one model, or you actually want to know what you spent.&lt;/p>
&lt;p>The setting that matters for this post is a custom model in &lt;code>~/.grok/config.toml&lt;/code>. Grok Build will talk to any OpenAI-compatible endpoint you give it a &lt;code>base_url&lt;/code> for. It asks for exactly one thing in return: a key.&lt;/p>
&lt;p>That&amp;rsquo;s the moment worth pausing on. A coding agent with shell access, asking for a provider key it will keep on disk — and once it has it, every call is between those two parties. No one else sees the traffic. Nothing counts it. Nothing constrains it.&lt;/p>
&lt;h2 id="and-what-is-agentgateway">And what is agentgateway?&lt;/h2>
&lt;p>&lt;strong>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/strong> is an open source connectivity data plane built specifically for agent traffic — LLM calls, MCP tool calls, and agent-to-agent. I&amp;rsquo;ve written about &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">why that needs to be its own thing&lt;/a> rather than a reverse proxy with extra steps.&lt;/p>
&lt;p>For this post the relevant part is simple: it&amp;rsquo;s a single binary that speaks the OpenAI &lt;code>/v1&lt;/code> dialect. Anything that can call OpenAI can call it instead — including Grok Build, and including curl. The client never holds the real key.&lt;/p>
&lt;h2 id="what-were-actually-here-to-do">What we&amp;rsquo;re actually here to do&lt;/h2>
&lt;p>Put the gateway between Grok Build and xAI, and that one hop becomes the place where five things live that the agent has no opinion about:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Govern&lt;/strong> — one place that decides what this client is allowed to ask for. Virtual keys, prompt guards, rate limits, spend caps.&lt;/li>
&lt;li>&lt;strong>Secure&lt;/strong> — the real &lt;code>XAI_API_KEY&lt;/code> stays inside the gateway process. Grok Build gets a token I invented, and it works just as well.&lt;/li>
&lt;li>&lt;strong>Route&lt;/strong> — Grok Build asks for &lt;code>grok-4-latest&lt;/code>; the gateway decides which provider and which exact model version actually serves it. Mine resolved to &lt;code>grok-4.3&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Log&lt;/strong> — every call, its status, and the model it resolved to, on one page. Without a proxy, this traffic is invisible.&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong> — tokens turned into dollars with a cost catalog, attributed per model and per provider.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ll build the standalone path we actually ran on one box. No cluster required. Secure, route, log, and cost come first, because you want to see traffic before you start refusing it. Governance is the same door getting pickier later — I walked that part in the &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness post&lt;/a> from today. This Saturday I wanted the hop working, and a receipt.&lt;/p>
&lt;p>The older kind runbook — client holds the key, gateway just forwards &lt;code>Authorization&lt;/code> — is a different path: &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/blob/main/docs/grok-passthrough-kind.md">&lt;code>docs/grok-passthrough-kind.md&lt;/code>&lt;/a>. Mentioning it so you don&amp;rsquo;t follow it by accident.&lt;/p>
&lt;p>By the end of it I asked two deliberately boring questions, and the gateway handed me a receipt: &lt;strong>$0.0044 / 623 tokens / 2 calls.&lt;/strong> A small amount of money, and exactly the point — that number simply does not exist when an agent talks to xAI directly.&lt;/p>
&lt;p>Here&amp;rsquo;s the whole build, including the five places I tripped.&lt;/p>
&lt;p>👉 Everything below is in the repo: &lt;strong>&lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway">sebbycorp/grok-build-with-agentgateway&lt;/a>&lt;/strong> · Never run the gateway binary before? Start with &lt;strong>&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">agentgateway standalone locally&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/curl-run.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/curl-run.png" alt="Two dummy-token curls through the gateway answering 4 and Paris" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="the-shape-of-it">The shape of it&lt;/h2>
&lt;p>Two processes on one laptop, and the only interesting part is which one holds the secret.&lt;/p>
&lt;div class="mermaid">flowchart LR
 grok[&amp;#34;Grok Build / curl&amp;#34;] --&amp;gt;|&amp;#34;dummy token /v1&amp;#34;| agw[&amp;#34;agentgateway :4003&amp;#34;]
 agw --&amp;gt;|&amp;#34;real XAI_API_KEY&amp;#34;| xai[&amp;#34;api.x.ai&amp;#34;]
 agw --&amp;gt; admin[&amp;#34;admin UI :14011&amp;#34;]
 agw --&amp;gt; jaeger[&amp;#34;Jaeger :16686&amp;#34;]
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hop&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>1 · Client&lt;/strong>&lt;/td>
&lt;td>Grok Build or curl hits &lt;code>http://127.0.0.1:4003/v1&lt;/code> with &lt;code>Authorization: Bearer local-grok-not-xai&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>2 · Gateway&lt;/strong>&lt;/td>
&lt;td>agentgateway accepts the dummy token and injects &lt;code>XAI_API_KEY&lt;/code> from process env. Tokens and cost get metered here.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>3 · Upstream&lt;/strong>&lt;/td>
&lt;td>Official &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/providers/xai/">xAI provider&lt;/a>. Default upstream is &lt;code>https://api.x.ai/v1&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4 · Receipt&lt;/strong>&lt;/td>
&lt;td>Admin UI on &lt;code>:14011&lt;/code>. Metrics on &lt;code>:14032&lt;/code>. Jaeger on &lt;code>:16686&lt;/code> if you started it.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Grok Build thinks it&amp;rsquo;s talking to xAI. It isn&amp;rsquo;t — it&amp;rsquo;s talking to a gateway on &lt;code>127.0.0.1:4003&lt;/code> that speaks the same OpenAI-compatible dialect, accepts my made-up token, and quietly swaps in the real key on the way out. Because every request passes through that one process, it&amp;rsquo;s also the natural place to count tokens, add up dollars, and emit traces.&lt;/p>
&lt;p>The rule I care about: &lt;strong>the key is not in GitHub, not in &lt;code>~/.grok&lt;/code>, and not in the Grok Build process.&lt;/strong> It lives in one file on disk with mode 600, and it gets loaded into exactly one process.&lt;/p>
&lt;p>Ports are &lt;strong>4003 / 14011&lt;/strong> on purpose, so this instance can sit next to an OpenAI gateway on &lt;code>4002 / 14010&lt;/code> — the &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness&lt;/a> / &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">OpenMausBot&lt;/a> lab. Same laptop, two providers, two doors.&lt;/p>
&lt;hr>
&lt;h2 id="standing-up-the-gateway">Standing up the gateway&lt;/h2>
&lt;p>Pinned to 1.4.1 — I&amp;rsquo;m on the &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">1.4 OSS line&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash -s -- --version v1.4.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, the piece people skip. The gateway can count tokens on its own, but tokens aren&amp;rsquo;t money. Import the cost catalog once and it can turn those counts into dollars:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p costs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agctl costs import --source models.dev --providers xai --out ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want traces too, Jaeger all-in-one is one command. Skip it if you don&amp;rsquo;t care — nothing else depends on it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name jaeger &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">COLLECTOR_OTLP_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 16686:16686 -p 4317:4317 -p 4318:4318 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> jaegertracing/all-in-one:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now the config. The thing to notice is what &lt;em>isn&amp;rsquo;t&lt;/em> in it — there&amp;rsquo;s no secret here, just a placeholder, which is why this file is safe to commit. Standalone has a first-class &lt;code>provider: xai&lt;/code>. Default upstream is &lt;code>https://api.x.ai/v1&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># yaml-language-server: $schema=https://agentgateway.dev/schema/config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost:14011&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">statsAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;[::]:14032&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;[::]:14033&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sqlite://./data.db?mode=rwc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./costs/catalog.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://localhost:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateways&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4003&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$XAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The sqlite URL is what makes Analytics and Logs actually store rows. Skip it and the admin UI looks empty even when the calls succeeded.&lt;/p>
&lt;p>The real key goes in its own file, locked down, and git never sees it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p .secrets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">umask&lt;/span> &lt;span class="m">077&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;export XAI_API_KEY=xai-...\n&amp;#39;&lt;/span> &amp;gt; .secrets/xai.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> .secrets/xai.env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And a small start script does the only clever thing in this whole setup — it sources that file into &lt;em>this process and nothing else&lt;/em>, then refuses to start if the key didn&amp;rsquo;t make it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ROOT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>dirname &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$0&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SECRET&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">AGW_SECRET_FILE&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="p">/.secrets/xai.env&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;missing &lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">XAI_API_KEY&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;XAI_API_KEY is empty after sourcing &lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> agentgateway -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="s2">/agentgateway.yaml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./start-agw.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;code>http://127.0.0.1:14011/ui&lt;/code> and make sure the admin UI is actually there before moving on. If the gateway isn&amp;rsquo;t running, everything in the next section fails in ways that look like Grok problems but aren&amp;rsquo;t.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-admin.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-admin.png" alt="agentgateway Gateway Overview with LLM enabled" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="two-boring-questions">Two boring questions&lt;/h2>
&lt;p>I kept the test deliberately dull, because I wasn&amp;rsquo;t testing the model — I was testing the plumbing. Dummy inbound token. Real key stays on the gateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS http://127.0.0.1:4003/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer local-grok-not-xai&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;grok-4-latest&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;What is 2+2? Reply with just the number.&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read that token again: &lt;code>local-grok-not-xai&lt;/code>. It isn&amp;rsquo;t a credential, it&amp;rsquo;s a label. The gateway is sitting on loopback and will accept it happily, then attach the real key on the way upstream.&lt;/p>
&lt;p>Then a second short turn so Analytics has more than one row:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS http://127.0.0.1:4003/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer local-grok-not-xai&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;grok-4-latest&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Name the capital of France in one word.&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This box: both returned &lt;strong>200&lt;/strong>. &lt;code>grok-4-latest&lt;/code> routed to &lt;code>grok-4.3&lt;/code>. Answers were &lt;code>4&lt;/code> and &lt;code>Paris&lt;/code>.&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;What is 2+2? Reply with just the number.&amp;rdquo;&lt;/em> → &lt;strong>4&lt;/strong>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Name the capital of France in one word.&amp;rdquo;&lt;/em> → &lt;strong>Paris&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>Four and Paris. Not exactly a demo you&amp;rsquo;d put on a conference slide. But those two answers travelled from a client holding a fake token, through a gateway holding the real one, out to xAI and back — which is the only thing I wanted to prove.&lt;/p>
&lt;p>Official agentgateway docs mention &lt;code>grok-2-latest&lt;/code>. This box used &lt;code>grok-4-latest&lt;/code>. If a model 404s, list what your account actually has through the same listener:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS http://127.0.0.1:4003/v1/models &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer local-grok-not-xai&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="a-receipt">A receipt&lt;/h2>
&lt;p>Now the part I actually built this for. Over in the admin UI, that conversation has a receipt: &lt;strong>$0.0044 / 623 tokens / 2 calls&lt;/strong>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-ui.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-ui.png" alt="agentgateway Analytics showing $0.0044, 623 tokens, and 2 calls" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The Logs page is the proof that the dummy token really did reach xAI and come back: two &lt;code>CHAT&lt;/code> rows, both &lt;code>200&lt;/code>, model routing resolving &lt;code>grok-4-latest&lt;/code> to &lt;code>grok-4.3&lt;/code> on provider &lt;code>xai&lt;/code>. And no key anywhere on those pages, which is the whole idea.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-logs.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-logs.png" alt="agentgateway Logs showing two CHAT 200 calls, grok-4-latest to grok-4.3, provider xai" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Scale that thought up. It&amp;rsquo;s a small bill for two short questions, but it&amp;rsquo;s the same counter when an agent runs unattended for an hour. If you want the richer version of this view, I went deeper on it in the &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">cost and tokenomics dashboard&lt;/a> post.&lt;/p>
&lt;hr>
&lt;h2 id="point-grok-build-at-the-same-door">Point Grok Build at the same door&lt;/h2>
&lt;p>Grok Build reads &lt;code>~/.grok/config.toml&lt;/code>. Add a custom model that talks to the gateway with the &lt;strong>dummy&lt;/strong> token, not &lt;code>XAI_API_KEY&lt;/code>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/grok-config.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/grok-config.png" alt="Grok Build config.toml pointed at the gateway with a dummy env_key" loading="lazy">
&lt;/a>
&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agw&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;grok-4-latest&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://127.0.0.1:4003/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">env_key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;GATEWAY_API_KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">models&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">default&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>local-grok-not-xai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grok
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or &lt;code>grok -m agw&lt;/code>. &lt;code>env_key&lt;/code> is the name of an env var Grok Build reads. That var is the dummy. The real xAI key never enters &lt;code>~/.grok&lt;/code>. Switch models in the TUI with &lt;code>/model&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="where-i-tripped">Where I tripped&lt;/h2>
&lt;p>Five failures, all of them mine, none of them the gateway&amp;rsquo;s fault. I&amp;rsquo;m writing them down because I&amp;rsquo;d have saved the evening if someone else had.&lt;/p>
&lt;p>&lt;strong>&lt;code>start-agw.sh&lt;/code> refused to start.&lt;/strong> I&amp;rsquo;d typed the YAML, I&amp;rsquo;d typed the curls, and the launcher said &lt;code>missing .secrets/xai.env&lt;/code>. The script is doing the only clever thing in this setup on purpose: it will not bring the process up if the mode-600 file is missing or if &lt;code>XAI_API_KEY&lt;/code> is empty after sourcing it. Redo the 600 file. Do not put the key in the YAML.&lt;/p>
&lt;p>&lt;strong>Then the curls came back 401 from upstream.&lt;/strong> Same class of mistake, later in the path. The dummy token was fine — &lt;code>local-grok-not-xai&lt;/code> is a label, not a credential. The real key was empty or wrong in the process env. Check the 600 file. Still do not put the key in the YAML.&lt;/p>
&lt;p>&lt;strong>Then a model 404.&lt;/strong> Official agentgateway docs mention &lt;code>grok-2-latest&lt;/code>. This box used &lt;code>grok-4-latest&lt;/code>. If the id isn&amp;rsquo;t on your xAI account, &lt;code>GET /v1/models&lt;/code> through the same listener and pick one you have.&lt;/p>
&lt;p>&lt;strong>Analytics and Logs stayed empty.&lt;/strong> The calls had returned 200. The admin UI looked like nothing had happened. Two causes, both boring: the client wasn&amp;rsquo;t actually hitting &lt;code>:4003&lt;/code>, or &lt;code>config.database&lt;/code> was missing. The sample YAML includes &lt;code>sqlite://./data.db?mode=rwc&lt;/code> for a reason. Recheck the base URL before you rebuild the catalog.&lt;/p>
&lt;p>&lt;strong>Port already in use.&lt;/strong> Another agentgateway was already sitting on &lt;code>4002 / 14010&lt;/code> — the OpenAI door from the DeepSeek / OpenMausBot lab. This config uses &lt;code>4003 / 14011&lt;/code> on purpose so they can share a laptop. If you copy the OpenAI YAML and forget to change the ports, the second process loses.&lt;/p>
&lt;p>That&amp;rsquo;s the entire troubleshooting section. Fix the file, fix the key, pick a model you have, keep the sqlite URL, stay off the other door.&lt;/p>
&lt;hr>
&lt;h2 id="the-same-trick-on-a-cluster">The same trick on a cluster&lt;/h2>
&lt;p>Nothing about the pattern changes when this moves off the laptop. Only the hiding place for the secret does — the mode-600 file becomes a Kubernetes Secret, and the local YAML becomes a handful of CRDs. The repo has them under &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/tree/main/k8s">&lt;code>k8s/&lt;/code>&lt;/a>, with a walkthrough in &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/blob/main/docs/kubernetes.md">&lt;code>docs/kubernetes.md&lt;/code>&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Being straight with you:&lt;/strong> the standalone path is the one I actually ran, and every screenshot above comes from it. The manifests exist. I didn&amp;rsquo;t stand a cluster up for this one, so treat them as a starting point rather than something I&amp;rsquo;ve proven. For a cluster walkthrough I &lt;em>have&lt;/em> run, see &lt;a href="https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/">proxying all your LLM traffic through agentgateway with Grok Build&lt;/a> and &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>.&lt;/p>
&lt;p>The older kind passthrough — client holds the key — is still &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/blob/main/docs/grok-passthrough-kind.md">&lt;code>docs/grok-passthrough-kind.md&lt;/code>&lt;/a>. Different runbook. Not this path.&lt;/p>
&lt;p>MCP isn&amp;rsquo;t wired into this one yet. Same gateway, later.&lt;/p>
&lt;hr>
&lt;h2 id="why-i-keep-doing-this">Why I keep doing this&lt;/h2>
&lt;p>The setup takes an evening. What I get back is the five things I opened with, and they&amp;rsquo;re all boring in the best way.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Without the gateway&lt;/th>
&lt;th>With this path&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Grok Build talks straight to &lt;code>api.x.ai&lt;/code> with a key in &lt;code>~/.grok&lt;/code>&lt;/td>
&lt;td>Dummy token in &lt;code>~/.grok&lt;/code>. Real &lt;code>XAI_API_KEY&lt;/code> lives in one process.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Nothing decides who may call, or what they may send&lt;/td>
&lt;td>The hop is where govern lives — virtual keys, guards, ceilings — when you want the door pickier&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>grok-4-latest&lt;/code> is whatever xAI serves today&lt;/td>
&lt;td>The gateway routes it. Mine landed on &lt;code>grok-4.3&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The traffic is invisible&lt;/td>
&lt;td>Two &lt;code>CHAT&lt;/code> / &lt;code>200&lt;/code> rows on Logs, provider &lt;code>xai&lt;/code>, no key on the page&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What did this cost?&amp;rdquo; shrugged off&lt;/td>
&lt;td>&lt;strong>$0.0044 / 623 tokens / 2 calls&lt;/strong> on Analytics&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The real key lives in one file, loaded by one process, and &lt;code>~/.grok&lt;/code> holds a dummy token that&amp;rsquo;s worthless anywhere else — &lt;strong>secure&lt;/strong>. Rotating the real one means editing a file and restarting a single thing. Grok Build asks for &lt;code>grok-4-latest&lt;/code> and the gateway decides what actually answers — &lt;strong>route&lt;/strong>. Every call is on a page with its status and resolved model — &lt;strong>log&lt;/strong>. When someone asks what an agent cost to run, I have a number instead of a shrug — &lt;strong>cost&lt;/strong>. And the door can get pickier later without the agent learning that any of this exists — &lt;strong>govern&lt;/strong>.&lt;/p>
&lt;p>That last one is the reason this is worth an evening rather than a &lt;code>curl&lt;/code>. The first four make the setup pleasant. Governance is what makes it something you can leave running while you&amp;rsquo;re asleep, or hand to someone who isn&amp;rsquo;t you. I flipped that on for &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness&lt;/a> today. The agent never learned that any of it happened.&lt;/p>
&lt;p>And notice where all of it lives: five controls, none of them inside Grok Build. The TUI never learned that any of this exists.&lt;/p>
&lt;p>The shape keeps repeating. Whether it&amp;rsquo;s &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code and Codex&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">OpenMausBot&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness&lt;/a>, or Grok Build, the app stays on loopback with a fake token and the secret stays in the gateway. Every new toy just points at &lt;code>/v1&lt;/code>.&lt;/p>
&lt;p>Which means the next one takes ten minutes, not an evening. That&amp;rsquo;s really why I bother.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Repo: &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway">sebbycorp/grok-build-with-agentgateway&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/providers/xai/">xAI in standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">How To: Run agentgateway standalone locally&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">How To: Point DeepSeek Harness at standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">How To: Point OpenMausBot at standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">How To: Connect Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/">How To: Point Claude Desktop at agentgateway with Entra SSO&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">agentgateway standalone cost &amp;amp; tokenomics dashboard&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/">Proxying all your LLM traffic through agentgateway with Grok Build&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">agentgateway 1.4 OSS: what changed from 1.3&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h2 id="first-what-is-grok-build">First, what is Grok Build?&lt;/h2>
&lt;p>&lt;strong>Grok Build&lt;/strong> is xAI&amp;rsquo;s coding agent. It lives in the terminal, reads the repo, edits files, and shells out. Out of the box it talks straight to &lt;code>api.x.ai&lt;/code> with a key sitting in &lt;code>~/.grok&lt;/code> or an env var.&lt;/p>
&lt;p>That&amp;rsquo;s fine for one laptop. It&amp;rsquo;s ugly the moment you have more than one tool, more than one model, or you actually want to know what you spent.&lt;/p>
&lt;p>The setting that matters for this post is a custom model in &lt;code>~/.grok/config.toml&lt;/code>. Grok Build will talk to any OpenAI-compatible endpoint you give it a &lt;code>base_url&lt;/code> for. It asks for exactly one thing in return: a key.&lt;/p>
&lt;p>That&amp;rsquo;s the moment worth pausing on. A coding agent with shell access, asking for a provider key it will keep on disk — and once it has it, every call is between those two parties. No one else sees the traffic. Nothing counts it. Nothing constrains it.&lt;/p>
&lt;h2 id="and-what-is-agentgateway">And what is agentgateway?&lt;/h2>
&lt;p>&lt;strong>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/strong> is an open source connectivity data plane built specifically for agent traffic — LLM calls, MCP tool calls, and agent-to-agent. I&amp;rsquo;ve written about &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">why that needs to be its own thing&lt;/a> rather than a reverse proxy with extra steps.&lt;/p>
&lt;p>For this post the relevant part is simple: it&amp;rsquo;s a single binary that speaks the OpenAI &lt;code>/v1&lt;/code> dialect. Anything that can call OpenAI can call it instead — including Grok Build, and including curl. The client never holds the real key.&lt;/p>
&lt;h2 id="what-were-actually-here-to-do">What we&amp;rsquo;re actually here to do&lt;/h2>
&lt;p>Put the gateway between Grok Build and xAI, and that one hop becomes the place where five things live that the agent has no opinion about:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Govern&lt;/strong> — one place that decides what this client is allowed to ask for. Virtual keys, prompt guards, rate limits, spend caps.&lt;/li>
&lt;li>&lt;strong>Secure&lt;/strong> — the real &lt;code>XAI_API_KEY&lt;/code> stays inside the gateway process. Grok Build gets a token I invented, and it works just as well.&lt;/li>
&lt;li>&lt;strong>Route&lt;/strong> — Grok Build asks for &lt;code>grok-4-latest&lt;/code>; the gateway decides which provider and which exact model version actually serves it. Mine resolved to &lt;code>grok-4.3&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Log&lt;/strong> — every call, its status, and the model it resolved to, on one page. Without a proxy, this traffic is invisible.&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong> — tokens turned into dollars with a cost catalog, attributed per model and per provider.&lt;/li>
&lt;/ul>
&lt;p>I&amp;rsquo;ll build the standalone path we actually ran on one box. No cluster required. Secure, route, log, and cost come first, because you want to see traffic before you start refusing it. Governance is the same door getting pickier later — I walked that part in the &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness post&lt;/a> from today. This Saturday I wanted the hop working, and a receipt.&lt;/p>
&lt;p>The older kind runbook — client holds the key, gateway just forwards &lt;code>Authorization&lt;/code> — is a different path: &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/blob/main/docs/grok-passthrough-kind.md">&lt;code>docs/grok-passthrough-kind.md&lt;/code>&lt;/a>. Mentioning it so you don&amp;rsquo;t follow it by accident.&lt;/p>
&lt;p>By the end of it I asked two deliberately boring questions, and the gateway handed me a receipt: &lt;strong>$0.0044 / 623 tokens / 2 calls.&lt;/strong> A small amount of money, and exactly the point — that number simply does not exist when an agent talks to xAI directly.&lt;/p>
&lt;p>Here&amp;rsquo;s the whole build, including the five places I tripped.&lt;/p>
&lt;p>👉 Everything below is in the repo: &lt;strong>&lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway">sebbycorp/grok-build-with-agentgateway&lt;/a>&lt;/strong> · Never run the gateway binary before? Start with &lt;strong>&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">agentgateway standalone locally&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/curl-run.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/curl-run.png" alt="Two dummy-token curls through the gateway answering 4 and Paris" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="the-shape-of-it">The shape of it&lt;/h2>
&lt;p>Two processes on one laptop, and the only interesting part is which one holds the secret.&lt;/p>
&lt;div class="mermaid">flowchart LR
 grok[&amp;#34;Grok Build / curl&amp;#34;] --&amp;gt;|&amp;#34;dummy token /v1&amp;#34;| agw[&amp;#34;agentgateway :4003&amp;#34;]
 agw --&amp;gt;|&amp;#34;real XAI_API_KEY&amp;#34;| xai[&amp;#34;api.x.ai&amp;#34;]
 agw --&amp;gt; admin[&amp;#34;admin UI :14011&amp;#34;]
 agw --&amp;gt; jaeger[&amp;#34;Jaeger :16686&amp;#34;]
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hop&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>1 · Client&lt;/strong>&lt;/td>
&lt;td>Grok Build or curl hits &lt;code>http://127.0.0.1:4003/v1&lt;/code> with &lt;code>Authorization: Bearer local-grok-not-xai&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>2 · Gateway&lt;/strong>&lt;/td>
&lt;td>agentgateway accepts the dummy token and injects &lt;code>XAI_API_KEY&lt;/code> from process env. Tokens and cost get metered here.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>3 · Upstream&lt;/strong>&lt;/td>
&lt;td>Official &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/providers/xai/">xAI provider&lt;/a>. Default upstream is &lt;code>https://api.x.ai/v1&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4 · Receipt&lt;/strong>&lt;/td>
&lt;td>Admin UI on &lt;code>:14011&lt;/code>. Metrics on &lt;code>:14032&lt;/code>. Jaeger on &lt;code>:16686&lt;/code> if you started it.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Grok Build thinks it&amp;rsquo;s talking to xAI. It isn&amp;rsquo;t — it&amp;rsquo;s talking to a gateway on &lt;code>127.0.0.1:4003&lt;/code> that speaks the same OpenAI-compatible dialect, accepts my made-up token, and quietly swaps in the real key on the way out. Because every request passes through that one process, it&amp;rsquo;s also the natural place to count tokens, add up dollars, and emit traces.&lt;/p>
&lt;p>The rule I care about: &lt;strong>the key is not in GitHub, not in &lt;code>~/.grok&lt;/code>, and not in the Grok Build process.&lt;/strong> It lives in one file on disk with mode 600, and it gets loaded into exactly one process.&lt;/p>
&lt;p>Ports are &lt;strong>4003 / 14011&lt;/strong> on purpose, so this instance can sit next to an OpenAI gateway on &lt;code>4002 / 14010&lt;/code> — the &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness&lt;/a> / &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">OpenMausBot&lt;/a> lab. Same laptop, two providers, two doors.&lt;/p>
&lt;hr>
&lt;h2 id="standing-up-the-gateway">Standing up the gateway&lt;/h2>
&lt;p>Pinned to 1.4.1 — I&amp;rsquo;m on the &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">1.4 OSS line&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash -s -- --version v1.4.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, the piece people skip. The gateway can count tokens on its own, but tokens aren&amp;rsquo;t money. Import the cost catalog once and it can turn those counts into dollars:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p costs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agctl costs import --source models.dev --providers xai --out ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you want traces too, Jaeger all-in-one is one command. Skip it if you don&amp;rsquo;t care — nothing else depends on it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name jaeger &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">COLLECTOR_OTLP_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 16686:16686 -p 4317:4317 -p 4318:4318 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> jaegertracing/all-in-one:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now the config. The thing to notice is what &lt;em>isn&amp;rsquo;t&lt;/em> in it — there&amp;rsquo;s no secret here, just a placeholder, which is why this file is safe to commit. Standalone has a first-class &lt;code>provider: xai&lt;/code>. Default upstream is &lt;code>https://api.x.ai/v1&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># yaml-language-server: $schema=https://agentgateway.dev/schema/config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localhost:14011&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">statsAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;[::]:14032&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;[::]:14033&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sqlite://./data.db?mode=rwc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./costs/catalog.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://localhost:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateways&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4003&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$XAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The sqlite URL is what makes Analytics and Logs actually store rows. Skip it and the admin UI looks empty even when the calls succeeded.&lt;/p>
&lt;p>The real key goes in its own file, locked down, and git never sees it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p .secrets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">umask&lt;/span> &lt;span class="m">077&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;export XAI_API_KEY=xai-...\n&amp;#39;&lt;/span> &amp;gt; .secrets/xai.env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod &lt;span class="m">600&lt;/span> .secrets/xai.env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And a small start script does the only clever thing in this whole setup — it sources that file into &lt;em>this process and nothing else&lt;/em>, then refuses to start if the key didn&amp;rsquo;t make it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ROOT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>dirname &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$0&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SECRET&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">AGW_SECRET_FILE&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="p">/.secrets/xai.env&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;missing &lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">XAI_API_KEY&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;XAI_API_KEY is empty after sourcing &lt;/span>&lt;span class="nv">$SECRET&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &amp;gt;&lt;span class="p">&amp;amp;&lt;/span>2&lt;span class="p">;&lt;/span> &lt;span class="nb">exit&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> agentgateway -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ROOT&lt;/span>&lt;span class="s2">/agentgateway.yaml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./start-agw.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;code>http://127.0.0.1:14011/ui&lt;/code> and make sure the admin UI is actually there before moving on. If the gateway isn&amp;rsquo;t running, everything in the next section fails in ways that look like Grok problems but aren&amp;rsquo;t.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-admin.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-admin.png" alt="agentgateway Gateway Overview with LLM enabled" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="two-boring-questions">Two boring questions&lt;/h2>
&lt;p>I kept the test deliberately dull, because I wasn&amp;rsquo;t testing the model — I was testing the plumbing. Dummy inbound token. Real key stays on the gateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS http://127.0.0.1:4003/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer local-grok-not-xai&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;grok-4-latest&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;What is 2+2? Reply with just the number.&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read that token again: &lt;code>local-grok-not-xai&lt;/code>. It isn&amp;rsquo;t a credential, it&amp;rsquo;s a label. The gateway is sitting on loopback and will accept it happily, then attach the real key on the way upstream.&lt;/p>
&lt;p>Then a second short turn so Analytics has more than one row:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS http://127.0.0.1:4003/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer local-grok-not-xai&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;grok-4-latest&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Name the capital of France in one word.&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This box: both returned &lt;strong>200&lt;/strong>. &lt;code>grok-4-latest&lt;/code> routed to &lt;code>grok-4.3&lt;/code>. Answers were &lt;code>4&lt;/code> and &lt;code>Paris&lt;/code>.&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;What is 2+2? Reply with just the number.&amp;rdquo;&lt;/em> → &lt;strong>4&lt;/strong>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Name the capital of France in one word.&amp;rdquo;&lt;/em> → &lt;strong>Paris&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>Four and Paris. Not exactly a demo you&amp;rsquo;d put on a conference slide. But those two answers travelled from a client holding a fake token, through a gateway holding the real one, out to xAI and back — which is the only thing I wanted to prove.&lt;/p>
&lt;p>Official agentgateway docs mention &lt;code>grok-2-latest&lt;/code>. This box used &lt;code>grok-4-latest&lt;/code>. If a model 404s, list what your account actually has through the same listener:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS http://127.0.0.1:4003/v1/models &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer local-grok-not-xai&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="a-receipt">A receipt&lt;/h2>
&lt;p>Now the part I actually built this for. Over in the admin UI, that conversation has a receipt: &lt;strong>$0.0044 / 623 tokens / 2 calls&lt;/strong>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-ui.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-ui.png" alt="agentgateway Analytics showing $0.0044, 623 tokens, and 2 calls" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The Logs page is the proof that the dummy token really did reach xAI and come back: two &lt;code>CHAT&lt;/code> rows, both &lt;code>200&lt;/code>, model routing resolving &lt;code>grok-4-latest&lt;/code> to &lt;code>grok-4.3&lt;/code> on provider &lt;code>xai&lt;/code>. And no key anywhere on those pages, which is the whole idea.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-logs.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/agw-logs.png" alt="agentgateway Logs showing two CHAT 200 calls, grok-4-latest to grok-4.3, provider xai" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Scale that thought up. It&amp;rsquo;s a small bill for two short questions, but it&amp;rsquo;s the same counter when an agent runs unattended for an hour. If you want the richer version of this view, I went deeper on it in the &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">cost and tokenomics dashboard&lt;/a> post.&lt;/p>
&lt;hr>
&lt;h2 id="point-grok-build-at-the-same-door">Point Grok Build at the same door&lt;/h2>
&lt;p>Grok Build reads &lt;code>~/.grok/config.toml&lt;/code>. Add a custom model that talks to the gateway with the &lt;strong>dummy&lt;/strong> token, not &lt;code>XAI_API_KEY&lt;/code>.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/grok-config.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-grok-build-standalone-agentgateway/grok-config.png" alt="Grok Build config.toml pointed at the gateway with a dummy env_key" loading="lazy">
&lt;/a>
&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agw&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;grok-4-latest&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://127.0.0.1:4003/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">env_key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;GATEWAY_API_KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">models&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">default&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>local-grok-not-xai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grok
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or &lt;code>grok -m agw&lt;/code>. &lt;code>env_key&lt;/code> is the name of an env var Grok Build reads. That var is the dummy. The real xAI key never enters &lt;code>~/.grok&lt;/code>. Switch models in the TUI with &lt;code>/model&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="where-i-tripped">Where I tripped&lt;/h2>
&lt;p>Five failures, all of them mine, none of them the gateway&amp;rsquo;s fault. I&amp;rsquo;m writing them down because I&amp;rsquo;d have saved the evening if someone else had.&lt;/p>
&lt;p>&lt;strong>&lt;code>start-agw.sh&lt;/code> refused to start.&lt;/strong> I&amp;rsquo;d typed the YAML, I&amp;rsquo;d typed the curls, and the launcher said &lt;code>missing .secrets/xai.env&lt;/code>. The script is doing the only clever thing in this setup on purpose: it will not bring the process up if the mode-600 file is missing or if &lt;code>XAI_API_KEY&lt;/code> is empty after sourcing it. Redo the 600 file. Do not put the key in the YAML.&lt;/p>
&lt;p>&lt;strong>Then the curls came back 401 from upstream.&lt;/strong> Same class of mistake, later in the path. The dummy token was fine — &lt;code>local-grok-not-xai&lt;/code> is a label, not a credential. The real key was empty or wrong in the process env. Check the 600 file. Still do not put the key in the YAML.&lt;/p>
&lt;p>&lt;strong>Then a model 404.&lt;/strong> Official agentgateway docs mention &lt;code>grok-2-latest&lt;/code>. This box used &lt;code>grok-4-latest&lt;/code>. If the id isn&amp;rsquo;t on your xAI account, &lt;code>GET /v1/models&lt;/code> through the same listener and pick one you have.&lt;/p>
&lt;p>&lt;strong>Analytics and Logs stayed empty.&lt;/strong> The calls had returned 200. The admin UI looked like nothing had happened. Two causes, both boring: the client wasn&amp;rsquo;t actually hitting &lt;code>:4003&lt;/code>, or &lt;code>config.database&lt;/code> was missing. The sample YAML includes &lt;code>sqlite://./data.db?mode=rwc&lt;/code> for a reason. Recheck the base URL before you rebuild the catalog.&lt;/p>
&lt;p>&lt;strong>Port already in use.&lt;/strong> Another agentgateway was already sitting on &lt;code>4002 / 14010&lt;/code> — the OpenAI door from the DeepSeek / OpenMausBot lab. This config uses &lt;code>4003 / 14011&lt;/code> on purpose so they can share a laptop. If you copy the OpenAI YAML and forget to change the ports, the second process loses.&lt;/p>
&lt;p>That&amp;rsquo;s the entire troubleshooting section. Fix the file, fix the key, pick a model you have, keep the sqlite URL, stay off the other door.&lt;/p>
&lt;hr>
&lt;h2 id="the-same-trick-on-a-cluster">The same trick on a cluster&lt;/h2>
&lt;p>Nothing about the pattern changes when this moves off the laptop. Only the hiding place for the secret does — the mode-600 file becomes a Kubernetes Secret, and the local YAML becomes a handful of CRDs. The repo has them under &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/tree/main/k8s">&lt;code>k8s/&lt;/code>&lt;/a>, with a walkthrough in &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/blob/main/docs/kubernetes.md">&lt;code>docs/kubernetes.md&lt;/code>&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Being straight with you:&lt;/strong> the standalone path is the one I actually ran, and every screenshot above comes from it. The manifests exist. I didn&amp;rsquo;t stand a cluster up for this one, so treat them as a starting point rather than something I&amp;rsquo;ve proven. For a cluster walkthrough I &lt;em>have&lt;/em> run, see &lt;a href="https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/">proxying all your LLM traffic through agentgateway with Grok Build&lt;/a> and &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>.&lt;/p>
&lt;p>The older kind passthrough — client holds the key — is still &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway/blob/main/docs/grok-passthrough-kind.md">&lt;code>docs/grok-passthrough-kind.md&lt;/code>&lt;/a>. Different runbook. Not this path.&lt;/p>
&lt;p>MCP isn&amp;rsquo;t wired into this one yet. Same gateway, later.&lt;/p>
&lt;hr>
&lt;h2 id="why-i-keep-doing-this">Why I keep doing this&lt;/h2>
&lt;p>The setup takes an evening. What I get back is the five things I opened with, and they&amp;rsquo;re all boring in the best way.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Without the gateway&lt;/th>
&lt;th>With this path&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Grok Build talks straight to &lt;code>api.x.ai&lt;/code> with a key in &lt;code>~/.grok&lt;/code>&lt;/td>
&lt;td>Dummy token in &lt;code>~/.grok&lt;/code>. Real &lt;code>XAI_API_KEY&lt;/code> lives in one process.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Nothing decides who may call, or what they may send&lt;/td>
&lt;td>The hop is where govern lives — virtual keys, guards, ceilings — when you want the door pickier&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>grok-4-latest&lt;/code> is whatever xAI serves today&lt;/td>
&lt;td>The gateway routes it. Mine landed on &lt;code>grok-4.3&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The traffic is invisible&lt;/td>
&lt;td>Two &lt;code>CHAT&lt;/code> / &lt;code>200&lt;/code> rows on Logs, provider &lt;code>xai&lt;/code>, no key on the page&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What did this cost?&amp;rdquo; shrugged off&lt;/td>
&lt;td>&lt;strong>$0.0044 / 623 tokens / 2 calls&lt;/strong> on Analytics&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The real key lives in one file, loaded by one process, and &lt;code>~/.grok&lt;/code> holds a dummy token that&amp;rsquo;s worthless anywhere else — &lt;strong>secure&lt;/strong>. Rotating the real one means editing a file and restarting a single thing. Grok Build asks for &lt;code>grok-4-latest&lt;/code> and the gateway decides what actually answers — &lt;strong>route&lt;/strong>. Every call is on a page with its status and resolved model — &lt;strong>log&lt;/strong>. When someone asks what an agent cost to run, I have a number instead of a shrug — &lt;strong>cost&lt;/strong>. And the door can get pickier later without the agent learning that any of this exists — &lt;strong>govern&lt;/strong>.&lt;/p>
&lt;p>That last one is the reason this is worth an evening rather than a &lt;code>curl&lt;/code>. The first four make the setup pleasant. Governance is what makes it something you can leave running while you&amp;rsquo;re asleep, or hand to someone who isn&amp;rsquo;t you. I flipped that on for &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness&lt;/a> today. The agent never learned that any of it happened.&lt;/p>
&lt;p>And notice where all of it lives: five controls, none of them inside Grok Build. The TUI never learned that any of this exists.&lt;/p>
&lt;p>The shape keeps repeating. Whether it&amp;rsquo;s &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code and Codex&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">OpenMausBot&lt;/a>, &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">DeepSeek Harness&lt;/a>, or Grok Build, the app stays on loopback with a fake token and the secret stays in the gateway. Every new toy just points at &lt;code>/v1&lt;/code>.&lt;/p>
&lt;p>Which means the next one takes ten minutes, not an evening. That&amp;rsquo;s really why I bother.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Repo: &lt;a href="https://github.com/sebbycorp/grok-build-with-agentgateway">sebbycorp/grok-build-with-agentgateway&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/providers/xai/">xAI in standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">How To: Run agentgateway standalone locally&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-15-deepseek-harness-standalone-agentgateway/">How To: Point DeepSeek Harness at standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/">How To: Point OpenMausBot at standalone agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">How To: Connect Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/">How To: Point Claude Desktop at agentgateway with Entra SSO&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">agentgateway standalone cost &amp;amp; tokenomics dashboard&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/">Proxying all your LLM traffic through agentgateway with Grok Build&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/">agentgateway 1.4 OSS: what changed from 1.3&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">agentgateway quickstart on Kubernetes&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>How To: Point OpenMausBot at standalone agentgateway</title><link>https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-15-openmausbot-standalone-agentgateway/</guid><description>&lt;p>I sat down on Saturday with &lt;a href="https://github.com/milind-soni/OpenMausBot">OpenMausBot&lt;/a> because I wanted a chat app that felt like texting a teammate, not another terminal. I named the bot &lt;strong>Indigo&lt;/strong>, picked &lt;strong>GPT-5.6 Sol&lt;/strong>, and asked it to say hi in five words.&lt;/p>
&lt;p>It did: &lt;strong>Hi, I&amp;rsquo;m Indigo, your bot.&lt;/strong>&lt;/p>
&lt;p>The part I loved — and the part that made me reach for &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> — is that OpenMausBot never called OpenAI. It spawned &lt;code>codex&lt;/code> on my laptop. That&amp;rsquo;s charming until you want a real key, a guardrail, a rate limit, or a cost number that isn&amp;rsquo;t &amp;ldquo;trust me, I watched the terminal.&amp;rdquo;&lt;/p>
&lt;p>So I did what I always do. Same pattern as &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code / Codex&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/">Claude Desktop&lt;/a>: &lt;strong>the chat app stays on loopback, the secret and the policy live in the gateway.&lt;/strong>&lt;/p>
&lt;p>This is that Saturday, written down so you can replay it.&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/milind-soni/OpenMausBot">milind-soni/OpenMausBot&lt;/a>&lt;/strong> · Official recipe: &lt;strong>&lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/codex/">Codex → agentgateway&lt;/a>&lt;/strong> · If you haven&amp;rsquo;t run the binary yet: &lt;strong>&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">standalone locally&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Think of it as three people in a room. OpenMausBot is the friendly one you talk to. Codex does the work. agentgateway is the grown-up who holds the wallet.&lt;/p>
&lt;div class="mermaid">flowchart LR
 UI[OpenMausBot UI&amp;lt;br/&amp;gt;127.0.0.1:5199] --&amp;gt;|HTTP + SSE| H[Harness&amp;lt;br/&amp;gt;spawns CLIs]
 H --&amp;gt;|codex app-server| CX[Codex 0.147.0]
 CX --&amp;gt;|Bearer virtual key&amp;lt;br/&amp;gt;http://127.0.0.1:4000/v1| GW[agentgateway :4000&amp;lt;br/&amp;gt;strict key · regex · rate · cost]
 GW --&amp;gt;|inject OPENAI_API_KEY| OA[api.openai.com]
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hop&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>1 · Chat&lt;/strong>&lt;/td>
&lt;td>I talk to &lt;strong>Indigo&lt;/strong> at &lt;code>http://127.0.0.1:5199&lt;/code>. The UI never sees a provider key — and that&amp;rsquo;s how I want it.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>2 · Spawn&lt;/strong>&lt;/td>
&lt;td>The harness starts &lt;code>codex&lt;/code>. The Codex driver &lt;strong>deletes &lt;code>OPENAI_API_KEY&lt;/code>&lt;/strong> from the child env on purpose, so a leaked key can&amp;rsquo;t quietly flip billing to pay-as-you-go.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>3 · Codex&lt;/strong>&lt;/td>
&lt;td>Codex 0.147.0 uses &lt;code>model_provider = &amp;quot;agentgateway&amp;quot;&lt;/code> and &lt;code>base_url = &amp;quot;http://127.0.0.1:4000/v1&amp;quot;&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4 · Gateway&lt;/strong>&lt;/td>
&lt;td>Virtual key, regex guard, token budget, cost catalog — then the real OpenAI key goes upstream.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Codex calls &lt;code>{base}/v1/responses&lt;/code> on the standalone LLM listener. Official docs were tested against &lt;code>codex-cli 0.144.4&lt;/code>. I was on &lt;strong>0.147.0&lt;/strong>. OpenMausBot&amp;rsquo;s default Codex model in this build is &lt;strong>GPT-5.6 Sol&lt;/strong>.&lt;/p>
&lt;p>I tried Claude first. It face-planted (&lt;code>exit 127&lt;/code>). Codex is the path that said hello.&lt;/p>
&lt;hr>
&lt;h2 id="why-openmausbot-never-holds-the-key">Why OpenMausBot never holds the key&lt;/h2>
&lt;p>This is the bit that made me smile when I read the source. Before it even starts the CLI, OpenMausBot strips the provider secrets:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Codex&lt;/strong> deletes &lt;code>OPENAI_API_KEY&lt;/code>&lt;/li>
&lt;li>&lt;strong>Claude&lt;/strong> deletes &lt;code>ANTHROPIC_API_KEY&lt;/code> (and the Claude Code identity vars)&lt;/li>
&lt;/ul>
&lt;p>You can paste a key into the OpenMausBot process and the bot still won&amp;rsquo;t send it. The CLI either uses its own login, or it talks to whatever &lt;code>base_url&lt;/code> you gave it.&lt;/p>
&lt;p>That&amp;rsquo;s my invitation. Standalone agentgateway &lt;em>is&lt;/em> that &lt;code>base_url&lt;/code>. One binary on my desk held the real &lt;code>OPENAI_API_KEY&lt;/code>, a virtual key tagged &lt;code>user: openmausbot&lt;/code> / &lt;code>tier: live&lt;/code>, the regex guards, the rate limit, and the cost catalog. Indigo didn&amp;rsquo;t need to know any of that.&lt;/p>
&lt;hr>
&lt;h2 id="what-was-actually-on-my-desk">What was actually on my desk&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Thing&lt;/th>
&lt;th>Saturday&amp;rsquo;s value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>OpenMausBot UI&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:5199&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Harness&lt;/td>
&lt;td>&lt;code>127.0.0.1:8799&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bot&lt;/td>
&lt;td>Indigo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model picker&lt;/td>
&lt;td>&lt;strong>GPT-5.6 Sol&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Codex&lt;/td>
&lt;td>&lt;code>0.147.0&lt;/code> · &lt;code>wire_api = &amp;quot;responses&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>agentgateway LLM&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:4000/v1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Admin UI&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:15000/ui/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Virtual key metadata&lt;/td>
&lt;td>&lt;code>user: openmausbot&lt;/code> · &lt;code>tier: live&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway Overview&lt;/td>
&lt;td>LLM &lt;strong>Enabled&lt;/strong> · &lt;strong>1 model&lt;/strong> · &lt;strong>Port 4000&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The prompt I used&lt;/td>
&lt;td>&lt;code>say hi in five words.&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What came back&lt;/td>
&lt;td>&lt;code>Hi, I'm Indigo, your bot.&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Sanity check whenever something feels off:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">agentgateway --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codex --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Once &lt;code>apiKey.mode&lt;/code> is &lt;code>strict&lt;/code>, a call with no key should &lt;strong>401&lt;/strong>. A call with the virtual key should reach OpenAI. That&amp;rsquo;s the whole handshake.&lt;/p>
&lt;hr>
&lt;h2 id="standalone-agentgateway">Standalone agentgateway&lt;/h2>
&lt;p>I installed the &lt;a href="https://agentgateway.dev/docs/standalone/latest/deployment/binary/">binary&lt;/a> and wrote a config that does the four jobs I actually wanted that afternoon: talk to OpenAI, know &lt;em>who&lt;/em> called, refuse sloppy secrets in the prompt, and put a number on the turn.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir -p ~/agw-openmausbot/costs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/agw-openmausbot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agctl costs import --source models.dev --providers openai --out ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &amp;gt; config.yaml &lt;span class="s">&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># yaml-language-server: $schema=https://agentgateway.dev/schema/config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> modelCatalog:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - file: ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">llm:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiKey:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mode: strict
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> keys:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - key: &amp;#34;$OPENMAUSBOT_VIRTUAL_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> user: openmausbot
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tier: live
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> localRateLimit:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - maxTokens: 200000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tokensPerFill: 200000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fillInterval: 3600s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> models:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: &amp;#34;*&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openAI
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> params:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiKey: &amp;#34;$OPENAI_API_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tokenize: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> guardrails:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - regex:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> action: reject
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - pattern: &amp;#34;api[_-]?key[=:]\\s*\\S+&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - pattern: &amp;#34;sk-[A-Za-z0-9_-]{10,}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - builtin: email
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rejection:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> status: 400
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> set:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> content-type: &amp;#34;application/json&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> body: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;error&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;message&amp;#34;: &amp;#34;Request rejected: sensitive content&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;type&amp;#34;: &amp;#34;invalid_request_error&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;code&amp;#34;: &amp;#34;content_policy_violation&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span> &lt;span class="c1"># gateway process only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENMAUSBOT_VIRTUAL_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-omb-...&amp;#39;&lt;/span> &lt;span class="c1"># what Codex presents; not an OpenAI key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway -f config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Port&lt;/th>
&lt;th>What I used it for&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>4000&lt;/strong>&lt;/td>
&lt;td>The front door Codex hits (&lt;code>/v1/responses&lt;/code>, &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/models&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>15000&lt;/strong>&lt;/td>
&lt;td>The Admin UI I left open in a tab — &lt;a href="http://127.0.0.1:15000/ui/">http://127.0.0.1:15000/ui/&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>15020&lt;/strong>&lt;/td>
&lt;td>Stats, if you like watching tokens (&lt;code>agentgateway_gen_ai_client_token_usage&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Leave it running. My Gateway Overview said &lt;strong>LLM Enabled&lt;/strong>, &lt;strong>1 model&lt;/strong>, &lt;strong>Port 4000&lt;/strong>. I didn&amp;rsquo;t turn on MCP or Traffic. I didn&amp;rsquo;t need them to say hello.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/agentgateway-overview.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/agentgateway-overview.png" alt="agentgateway Gateway Overview with LLM enabled, 1 model, and Port 4000" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Keep the model name as &lt;code>*&lt;/code>. Codex sends &lt;code>gpt-5.6-sol&lt;/code> itself. If you pin the gateway to one model, you&amp;rsquo;ll spend an hour wondering why Indigo looks confused.&lt;/p>
&lt;hr>
&lt;h2 id="virtual-api-key">Virtual API key&lt;/h2>
&lt;p>I didn&amp;rsquo;t want Codex holding the real OpenAI key. I wanted a &lt;em>house key&lt;/em> — something that says &amp;ldquo;this is OpenMausBot&amp;rdquo; when I look at cost later.&lt;/p>
&lt;p>&lt;strong>LLM → Virtual API Keys → + New key.&lt;/strong> I tagged mine like this:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>What I used&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Name&lt;/td>
&lt;td>I left it &lt;code>Unnamed key&lt;/code>. You can be nicer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>user&lt;/code>&lt;/td>
&lt;td>&lt;code>openmausbot&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tier&lt;/code>&lt;/td>
&lt;td>&lt;code>live&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Copy it once. The UI masks it (&lt;code>sk-omb-…&lt;/code>). That value opens &lt;em>your&lt;/em> gateway. It does nothing at OpenAI.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/virtual-api-keys.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/virtual-api-keys.png" alt="agentgateway Virtual API Keys page with an OpenMausBot key tagged user openmausbot and tier live" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>A few things I wish someone had said out loud:&lt;/p>
&lt;p>&lt;strong>Go &lt;code>strict&lt;/code> once you care who paid.&lt;/strong>&lt;br>
&lt;code>optional&lt;/code> is fine for the first &lt;code>curl&lt;/code>. The moment you want &lt;code>user: openmausbot&lt;/code> on the cost page, &lt;code>optional&lt;/code> is just a hole.&lt;/p>
&lt;p>&lt;strong>Don&amp;rsquo;t name the virtual key &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/strong>&lt;br>
OpenMausBot deletes that name before Codex starts. I put it in &lt;code>OPENMAUSBOT_VIRTUAL_KEY&lt;/code> and told Codex via &lt;code>env_key&lt;/code>. The harness can inherit it. Codex still sees it after spawn.&lt;/p>
&lt;p>&lt;strong>Client Setup is a printer, not a wizard.&lt;/strong>&lt;br>
&lt;a href="https://agentgateway.dev/docs/standalone/latest/operations/ui/#generate-llm-client-settings">Admin UI → Client Setup&lt;/a> will copy the Codex snippet from config you already wrote. It will not invent a route, a model, or a key. I still like it — less typing, fewer typos.&lt;/p>
&lt;p>More in the &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/cost-controls/virtual-keys/">virtual key docs&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="guardrails-rate-limits-cost">Guardrails, rate limits, cost&lt;/h2>
&lt;p>This is why I didn&amp;rsquo;t just export a key and call it a day. The YAML above is what I actually ran:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Control&lt;/th>
&lt;th>Why I put it there&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Regex &lt;code>reject&lt;/code>&lt;/td>
&lt;td>I don&amp;rsquo;t want a prompt that looks like &lt;code>api_key=…&lt;/code>, &lt;code>sk-…&lt;/code>, or an email to leave the laptop (&lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/prompt-guards/regex/">regex filters&lt;/a>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>localRateLimit&lt;/code> &lt;code>type: tokens&lt;/code>&lt;/td>
&lt;td>200k tokens an hour — enough for a Saturday, not enough for a runaway loop&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tokenize: true&lt;/code>&lt;/td>
&lt;td>Count the prompt &lt;em>before&lt;/em> OpenAI sees it, so a fat turn can 429 without spending&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>modelCatalog&lt;/code>&lt;/td>
&lt;td>So each Indigo turn has a dollar number (&lt;code>agw.ai.usage.cost.total&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>On standalone, that rate limit is for the whole gateway. If you later want per-key daily budgets, you&amp;rsquo;ll want a remote rate-limit server — &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/cost-controls/budget-limits/">budget limits&lt;/a> and my &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">hard spend limits&lt;/a> post. Cost catalog walkthrough: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">standalone dashboard&lt;/a>. If you want a heavier scanner in front of the same door: &lt;a href="https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/">F5 guardrails&lt;/a>.&lt;/p>
&lt;p>None of that YAML lives in OpenMausBot. Indigo just chats. I like that.&lt;/p>
&lt;hr>
&lt;h2 id="codex--4000v1">Codex → &lt;code>:4000/v1&lt;/code>&lt;/h2>
&lt;p>This is the &lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/codex/">official Codex recipe&lt;/a>. I wrote a profile, then made it the default so the CLI OpenMausBot spawns would pick it up without me thinking about it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p ~/.codex
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;gt; ~/.codex/agentgateway.config.toml &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">model_provider = &amp;#34;agentgateway&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">[model_providers.agentgateway]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">name = &amp;#34;OpenAI via agentgateway&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">base_url = &amp;#34;http://127.0.0.1:4000/v1&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">wire_api = &amp;#34;responses&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">env_key = &amp;#34;OPENMAUSBOT_VIRTUAL_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp ~/.codex/config.toml ~/.codex/config.toml.bak 2&amp;gt;/dev/null &lt;span class="o">||&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp ~/.codex/agentgateway.config.toml ~/.codex/config.toml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you&amp;rsquo;d rather not touch the default file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">codex -c &lt;span class="s1">&amp;#39;model_provider=&amp;#34;agentgateway&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.name=&amp;#34;OpenAI via agentgateway&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.base_url=&amp;#34;http://127.0.0.1:4000/v1&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.wire_api=&amp;#34;responses&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.env_key=&amp;#34;OPENMAUSBOT_VIRTUAL_KEY&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The three gotchas that ate my morning:&lt;/p>
&lt;p>&lt;strong>&lt;code>wire_api = &amp;quot;responses&amp;quot;&lt;/code>.&lt;/strong>&lt;br>
Codex speaks &lt;code>/v1/responses&lt;/code>, not only chat completions. Miss this and you&amp;rsquo;ll stare at a quiet gateway log while the CLI looks busy.&lt;/p>
&lt;p>&lt;strong>&lt;code>env_key&lt;/code> cannot be &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/strong>&lt;br>
That name is gone by the time Codex starts. &lt;code>OPENMAUSBOT_VIRTUAL_KEY&lt;/code> survives.&lt;/p>
&lt;p>&lt;strong>Codex also knocks on &lt;code>/v1/models&lt;/code>.&lt;/strong>&lt;br>
Until &lt;a href="https://github.com/agentgateway/agentgateway/issues/1462">agentgateway#1462&lt;/a> grows a model list, you may get a metadata warning. Ignore it. &lt;code>/v1/responses&lt;/code> still works.&lt;/p>
&lt;p>I always smoke-test the CLI &lt;em>before&lt;/em> I open the pretty chat UI. Saves a lot of &amp;ldquo;is it the bot or the gateway?&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENMAUSBOT_VIRTUAL_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-omb-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codex --profile agentgateway &lt;span class="s2">&amp;#34;Hello&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You want a &lt;code>200&lt;/code> on &lt;code>/v1/responses&lt;/code> in the agentgateway log (&lt;code>endpoint=api.openai.com:443&lt;/code>, &lt;code>gen_ai.provider.name=openai&lt;/code>). If Codex logs into ChatGPT instead, the profile didn&amp;rsquo;t load. Coffee, then check &lt;code>~/.codex/config.toml&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="talking-to-indigo">Talking to Indigo&lt;/h2>
&lt;p>Desktop builds embed the harness. From source I run both:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/milind-soni/OpenMausBot &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> OpenMausBot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pnpm install
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENMAUSBOT_VIRTUAL_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-omb-...&amp;#39;&lt;/span> &lt;span class="c1"># virtual key only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pnpm dev:server &lt;span class="c1"># 127.0.0.1:8799&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pnpm dev &lt;span class="c1"># http://127.0.0.1:5199&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, in the browser:&lt;/p>
&lt;ol>
&lt;li>Open &lt;code>http://127.0.0.1:5199&lt;/code>&lt;/li>
&lt;li>Click &lt;strong>Indigo&lt;/strong>&lt;/li>
&lt;li>Model picker → &lt;strong>GPT-5.6 Sol&lt;/strong>&lt;/li>
&lt;li>Type &lt;code>say hi in five words.&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>I picked the categories it offered (Work &amp;amp; projects — I was not in a &amp;ldquo;life admin&amp;rdquo; mood). Claude failed first, which I&amp;rsquo;ll get to. Then Codex came back through the gateway and I grinned at five words.&lt;/p>
&lt;p>&lt;strong>Hi, I&amp;rsquo;m Indigo, your bot.&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/openmausbot-indigo-chat.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/openmausbot-indigo-chat.png" alt="OpenMausBot Indigo chat on 127.0.0.1:5199 — Claude wrapper error, then a five-word Codex reply through agentgateway" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="the-claude-face-plant">The Claude face-plant&lt;/h2>
&lt;p>The first bubble in that thread was a red box. I left it in the screenshot on purpose. Saturday labs should be honest.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">claude exited 127 before result: openmausbot-agw wrapper: real claude CLI not installed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ANTHROPIC_BASE_URL=http://127.0.0.1:4000 (agentgateway)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>127&lt;/code> means &amp;ldquo;I can&amp;rsquo;t find that command.&amp;rdquo; A wrapper was already pointing Claude at agentgateway — the redirect was right — but I hadn&amp;rsquo;t actually installed &lt;code>claude&lt;/code>. OpenMausBot will not invent an Anthropic client because you wished for one.&lt;/p>
&lt;p>If you want that path later, install the CLI and keep &lt;code>ANTHROPIC_API_KEY&lt;/code> on the gateway. The Claude driver deletes it from the child the same way Codex deletes &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># after `claude` is actually installed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_BASE_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;http://127.0.0.1:4000&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I stayed on Codex. Five words were enough for the day.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">Why bother&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Without the gateway&lt;/th>
&lt;th>How Saturday felt with it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>OpenAI key on the laptop, or worse, in the bot&lt;/td>
&lt;td>The key stayed in the agentgateway process. I slept better.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Codex talking to OpenAI with no shared rules&lt;/td>
&lt;td>Virtual key, regex guards, a rate limit I can change without touching Indigo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What did this cost?&amp;rdquo; shrugged off&lt;/td>
&lt;td>Cost catalog + &lt;code>user: openmausbot&lt;/code> on the turn&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Every new chat app grows another secret&lt;/td>
&lt;td>One &lt;code>:4000&lt;/code> door. Next weekend&amp;rsquo;s toy can use it too.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you already send Claude Code or Codex through agentgateway, this is the same idea with a nicer living room: localhost, &lt;code>/v1&lt;/code>, a bot named Indigo instead of a blinking cursor.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/codex/">Codex → agentgateway&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/cost-controls/virtual-keys/">Virtual keys&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/prompt-guards/regex/">Regex guardrails&lt;/a>&lt;/li>
&lt;li>OpenMausBot: &lt;a href="https://github.com/milind-soni/OpenMausBot">milind-soni/OpenMausBot&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">How To: Run agentgateway standalone locally&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">How To: Connect Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/">How To: Point Claude Desktop at agentgateway with Entra SSO&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/">Route MCP / Claude traffic through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">agentgateway standalone cost &amp;amp; tokenomics dashboard&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">Hard spend limits for LLM traffic&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/">First steps: agentgateway and F5 AI Guardrails&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>I sat down on Saturday with &lt;a href="https://github.com/milind-soni/OpenMausBot">OpenMausBot&lt;/a> because I wanted a chat app that felt like texting a teammate, not another terminal. I named the bot &lt;strong>Indigo&lt;/strong>, picked &lt;strong>GPT-5.6 Sol&lt;/strong>, and asked it to say hi in five words.&lt;/p>
&lt;p>It did: &lt;strong>Hi, I&amp;rsquo;m Indigo, your bot.&lt;/strong>&lt;/p>
&lt;p>The part I loved — and the part that made me reach for &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> — is that OpenMausBot never called OpenAI. It spawned &lt;code>codex&lt;/code> on my laptop. That&amp;rsquo;s charming until you want a real key, a guardrail, a rate limit, or a cost number that isn&amp;rsquo;t &amp;ldquo;trust me, I watched the terminal.&amp;rdquo;&lt;/p>
&lt;p>So I did what I always do. Same pattern as &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code / Codex&lt;/a> and &lt;a href="https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/">Claude Desktop&lt;/a>: &lt;strong>the chat app stays on loopback, the secret and the policy live in the gateway.&lt;/strong>&lt;/p>
&lt;p>This is that Saturday, written down so you can replay it.&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/milind-soni/OpenMausBot">milind-soni/OpenMausBot&lt;/a>&lt;/strong> · Official recipe: &lt;strong>&lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/codex/">Codex → agentgateway&lt;/a>&lt;/strong> · If you haven&amp;rsquo;t run the binary yet: &lt;strong>&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">standalone locally&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Think of it as three people in a room. OpenMausBot is the friendly one you talk to. Codex does the work. agentgateway is the grown-up who holds the wallet.&lt;/p>
&lt;div class="mermaid">flowchart LR
 UI[OpenMausBot UI&amp;lt;br/&amp;gt;127.0.0.1:5199] --&amp;gt;|HTTP + SSE| H[Harness&amp;lt;br/&amp;gt;spawns CLIs]
 H --&amp;gt;|codex app-server| CX[Codex 0.147.0]
 CX --&amp;gt;|Bearer virtual key&amp;lt;br/&amp;gt;http://127.0.0.1:4000/v1| GW[agentgateway :4000&amp;lt;br/&amp;gt;strict key · regex · rate · cost]
 GW --&amp;gt;|inject OPENAI_API_KEY| OA[api.openai.com]
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hop&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>1 · Chat&lt;/strong>&lt;/td>
&lt;td>I talk to &lt;strong>Indigo&lt;/strong> at &lt;code>http://127.0.0.1:5199&lt;/code>. The UI never sees a provider key — and that&amp;rsquo;s how I want it.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>2 · Spawn&lt;/strong>&lt;/td>
&lt;td>The harness starts &lt;code>codex&lt;/code>. The Codex driver &lt;strong>deletes &lt;code>OPENAI_API_KEY&lt;/code>&lt;/strong> from the child env on purpose, so a leaked key can&amp;rsquo;t quietly flip billing to pay-as-you-go.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>3 · Codex&lt;/strong>&lt;/td>
&lt;td>Codex 0.147.0 uses &lt;code>model_provider = &amp;quot;agentgateway&amp;quot;&lt;/code> and &lt;code>base_url = &amp;quot;http://127.0.0.1:4000/v1&amp;quot;&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4 · Gateway&lt;/strong>&lt;/td>
&lt;td>Virtual key, regex guard, token budget, cost catalog — then the real OpenAI key goes upstream.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Codex calls &lt;code>{base}/v1/responses&lt;/code> on the standalone LLM listener. Official docs were tested against &lt;code>codex-cli 0.144.4&lt;/code>. I was on &lt;strong>0.147.0&lt;/strong>. OpenMausBot&amp;rsquo;s default Codex model in this build is &lt;strong>GPT-5.6 Sol&lt;/strong>.&lt;/p>
&lt;p>I tried Claude first. It face-planted (&lt;code>exit 127&lt;/code>). Codex is the path that said hello.&lt;/p>
&lt;hr>
&lt;h2 id="why-openmausbot-never-holds-the-key">Why OpenMausBot never holds the key&lt;/h2>
&lt;p>This is the bit that made me smile when I read the source. Before it even starts the CLI, OpenMausBot strips the provider secrets:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Codex&lt;/strong> deletes &lt;code>OPENAI_API_KEY&lt;/code>&lt;/li>
&lt;li>&lt;strong>Claude&lt;/strong> deletes &lt;code>ANTHROPIC_API_KEY&lt;/code> (and the Claude Code identity vars)&lt;/li>
&lt;/ul>
&lt;p>You can paste a key into the OpenMausBot process and the bot still won&amp;rsquo;t send it. The CLI either uses its own login, or it talks to whatever &lt;code>base_url&lt;/code> you gave it.&lt;/p>
&lt;p>That&amp;rsquo;s my invitation. Standalone agentgateway &lt;em>is&lt;/em> that &lt;code>base_url&lt;/code>. One binary on my desk held the real &lt;code>OPENAI_API_KEY&lt;/code>, a virtual key tagged &lt;code>user: openmausbot&lt;/code> / &lt;code>tier: live&lt;/code>, the regex guards, the rate limit, and the cost catalog. Indigo didn&amp;rsquo;t need to know any of that.&lt;/p>
&lt;hr>
&lt;h2 id="what-was-actually-on-my-desk">What was actually on my desk&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Thing&lt;/th>
&lt;th>Saturday&amp;rsquo;s value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>OpenMausBot UI&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:5199&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Harness&lt;/td>
&lt;td>&lt;code>127.0.0.1:8799&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bot&lt;/td>
&lt;td>Indigo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model picker&lt;/td>
&lt;td>&lt;strong>GPT-5.6 Sol&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Codex&lt;/td>
&lt;td>&lt;code>0.147.0&lt;/code> · &lt;code>wire_api = &amp;quot;responses&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>agentgateway LLM&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:4000/v1&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Admin UI&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:15000/ui/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Virtual key metadata&lt;/td>
&lt;td>&lt;code>user: openmausbot&lt;/code> · &lt;code>tier: live&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway Overview&lt;/td>
&lt;td>LLM &lt;strong>Enabled&lt;/strong> · &lt;strong>1 model&lt;/strong> · &lt;strong>Port 4000&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The prompt I used&lt;/td>
&lt;td>&lt;code>say hi in five words.&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>What came back&lt;/td>
&lt;td>&lt;code>Hi, I'm Indigo, your bot.&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Sanity check whenever something feels off:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">agentgateway --version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codex --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Once &lt;code>apiKey.mode&lt;/code> is &lt;code>strict&lt;/code>, a call with no key should &lt;strong>401&lt;/strong>. A call with the virtual key should reach OpenAI. That&amp;rsquo;s the whole handshake.&lt;/p>
&lt;hr>
&lt;h2 id="standalone-agentgateway">Standalone agentgateway&lt;/h2>
&lt;p>I installed the &lt;a href="https://agentgateway.dev/docs/standalone/latest/deployment/binary/">binary&lt;/a> and wrote a config that does the four jobs I actually wanted that afternoon: talk to OpenAI, know &lt;em>who&lt;/em> called, refuse sloppy secrets in the prompt, and put a number on the turn.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir -p ~/agw-openmausbot/costs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/agw-openmausbot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agctl costs import --source models.dev --providers openai --out ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &amp;gt; config.yaml &lt;span class="s">&amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># yaml-language-server: $schema=https://agentgateway.dev/schema/config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> modelCatalog:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - file: ./costs/catalog.json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">llm:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiKey:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mode: strict
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> keys:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - key: &amp;#34;$OPENMAUSBOT_VIRTUAL_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> user: openmausbot
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tier: live
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> localRateLimit:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - maxTokens: 200000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tokensPerFill: 200000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fillInterval: 3600s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> models:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: &amp;#34;*&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openAI
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> params:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiKey: &amp;#34;$OPENAI_API_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tokenize: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> guardrails:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - regex:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> action: reject
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - pattern: &amp;#34;api[_-]?key[=:]\\s*\\S+&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - pattern: &amp;#34;sk-[A-Za-z0-9_-]{10,}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - builtin: email
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rejection:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> status: 400
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> set:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> content-type: &amp;#34;application/json&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> body: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;error&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;message&amp;#34;: &amp;#34;Request rejected: sensitive content&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;type&amp;#34;: &amp;#34;invalid_request_error&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;code&amp;#34;: &amp;#34;content_policy_violation&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span> &lt;span class="c1"># gateway process only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENMAUSBOT_VIRTUAL_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-omb-...&amp;#39;&lt;/span> &lt;span class="c1"># what Codex presents; not an OpenAI key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway -f config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Port&lt;/th>
&lt;th>What I used it for&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>4000&lt;/strong>&lt;/td>
&lt;td>The front door Codex hits (&lt;code>/v1/responses&lt;/code>, &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/models&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>15000&lt;/strong>&lt;/td>
&lt;td>The Admin UI I left open in a tab — &lt;a href="http://127.0.0.1:15000/ui/">http://127.0.0.1:15000/ui/&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>15020&lt;/strong>&lt;/td>
&lt;td>Stats, if you like watching tokens (&lt;code>agentgateway_gen_ai_client_token_usage&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Leave it running. My Gateway Overview said &lt;strong>LLM Enabled&lt;/strong>, &lt;strong>1 model&lt;/strong>, &lt;strong>Port 4000&lt;/strong>. I didn&amp;rsquo;t turn on MCP or Traffic. I didn&amp;rsquo;t need them to say hello.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/agentgateway-overview.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/agentgateway-overview.png" alt="agentgateway Gateway Overview with LLM enabled, 1 model, and Port 4000" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Keep the model name as &lt;code>*&lt;/code>. Codex sends &lt;code>gpt-5.6-sol&lt;/code> itself. If you pin the gateway to one model, you&amp;rsquo;ll spend an hour wondering why Indigo looks confused.&lt;/p>
&lt;hr>
&lt;h2 id="virtual-api-key">Virtual API key&lt;/h2>
&lt;p>I didn&amp;rsquo;t want Codex holding the real OpenAI key. I wanted a &lt;em>house key&lt;/em> — something that says &amp;ldquo;this is OpenMausBot&amp;rdquo; when I look at cost later.&lt;/p>
&lt;p>&lt;strong>LLM → Virtual API Keys → + New key.&lt;/strong> I tagged mine like this:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>What I used&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Name&lt;/td>
&lt;td>I left it &lt;code>Unnamed key&lt;/code>. You can be nicer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>user&lt;/code>&lt;/td>
&lt;td>&lt;code>openmausbot&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tier&lt;/code>&lt;/td>
&lt;td>&lt;code>live&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Copy it once. The UI masks it (&lt;code>sk-omb-…&lt;/code>). That value opens &lt;em>your&lt;/em> gateway. It does nothing at OpenAI.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/virtual-api-keys.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/virtual-api-keys.png" alt="agentgateway Virtual API Keys page with an OpenMausBot key tagged user openmausbot and tier live" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>A few things I wish someone had said out loud:&lt;/p>
&lt;p>&lt;strong>Go &lt;code>strict&lt;/code> once you care who paid.&lt;/strong>&lt;br>
&lt;code>optional&lt;/code> is fine for the first &lt;code>curl&lt;/code>. The moment you want &lt;code>user: openmausbot&lt;/code> on the cost page, &lt;code>optional&lt;/code> is just a hole.&lt;/p>
&lt;p>&lt;strong>Don&amp;rsquo;t name the virtual key &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/strong>&lt;br>
OpenMausBot deletes that name before Codex starts. I put it in &lt;code>OPENMAUSBOT_VIRTUAL_KEY&lt;/code> and told Codex via &lt;code>env_key&lt;/code>. The harness can inherit it. Codex still sees it after spawn.&lt;/p>
&lt;p>&lt;strong>Client Setup is a printer, not a wizard.&lt;/strong>&lt;br>
&lt;a href="https://agentgateway.dev/docs/standalone/latest/operations/ui/#generate-llm-client-settings">Admin UI → Client Setup&lt;/a> will copy the Codex snippet from config you already wrote. It will not invent a route, a model, or a key. I still like it — less typing, fewer typos.&lt;/p>
&lt;p>More in the &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/cost-controls/virtual-keys/">virtual key docs&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="guardrails-rate-limits-cost">Guardrails, rate limits, cost&lt;/h2>
&lt;p>This is why I didn&amp;rsquo;t just export a key and call it a day. The YAML above is what I actually ran:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Control&lt;/th>
&lt;th>Why I put it there&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Regex &lt;code>reject&lt;/code>&lt;/td>
&lt;td>I don&amp;rsquo;t want a prompt that looks like &lt;code>api_key=…&lt;/code>, &lt;code>sk-…&lt;/code>, or an email to leave the laptop (&lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/prompt-guards/regex/">regex filters&lt;/a>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>localRateLimit&lt;/code> &lt;code>type: tokens&lt;/code>&lt;/td>
&lt;td>200k tokens an hour — enough for a Saturday, not enough for a runaway loop&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>tokenize: true&lt;/code>&lt;/td>
&lt;td>Count the prompt &lt;em>before&lt;/em> OpenAI sees it, so a fat turn can 429 without spending&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>modelCatalog&lt;/code>&lt;/td>
&lt;td>So each Indigo turn has a dollar number (&lt;code>agw.ai.usage.cost.total&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>On standalone, that rate limit is for the whole gateway. If you later want per-key daily budgets, you&amp;rsquo;ll want a remote rate-limit server — &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/cost-controls/budget-limits/">budget limits&lt;/a> and my &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">hard spend limits&lt;/a> post. Cost catalog walkthrough: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">standalone dashboard&lt;/a>. If you want a heavier scanner in front of the same door: &lt;a href="https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/">F5 guardrails&lt;/a>.&lt;/p>
&lt;p>None of that YAML lives in OpenMausBot. Indigo just chats. I like that.&lt;/p>
&lt;hr>
&lt;h2 id="codex--4000v1">Codex → &lt;code>:4000/v1&lt;/code>&lt;/h2>
&lt;p>This is the &lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/codex/">official Codex recipe&lt;/a>. I wrote a profile, then made it the default so the CLI OpenMausBot spawns would pick it up without me thinking about it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">mkdir -p ~/.codex
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &amp;gt; ~/.codex/agentgateway.config.toml &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">model_provider = &amp;#34;agentgateway&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">[model_providers.agentgateway]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">name = &amp;#34;OpenAI via agentgateway&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">base_url = &amp;#34;http://127.0.0.1:4000/v1&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">wire_api = &amp;#34;responses&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">env_key = &amp;#34;OPENMAUSBOT_VIRTUAL_KEY&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp ~/.codex/config.toml ~/.codex/config.toml.bak 2&amp;gt;/dev/null &lt;span class="o">||&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp ~/.codex/agentgateway.config.toml ~/.codex/config.toml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you&amp;rsquo;d rather not touch the default file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">codex -c &lt;span class="s1">&amp;#39;model_provider=&amp;#34;agentgateway&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.name=&amp;#34;OpenAI via agentgateway&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.base_url=&amp;#34;http://127.0.0.1:4000/v1&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.wire_api=&amp;#34;responses&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -c &lt;span class="s1">&amp;#39;model_providers.agentgateway.env_key=&amp;#34;OPENMAUSBOT_VIRTUAL_KEY&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The three gotchas that ate my morning:&lt;/p>
&lt;p>&lt;strong>&lt;code>wire_api = &amp;quot;responses&amp;quot;&lt;/code>.&lt;/strong>&lt;br>
Codex speaks &lt;code>/v1/responses&lt;/code>, not only chat completions. Miss this and you&amp;rsquo;ll stare at a quiet gateway log while the CLI looks busy.&lt;/p>
&lt;p>&lt;strong>&lt;code>env_key&lt;/code> cannot be &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/strong>&lt;br>
That name is gone by the time Codex starts. &lt;code>OPENMAUSBOT_VIRTUAL_KEY&lt;/code> survives.&lt;/p>
&lt;p>&lt;strong>Codex also knocks on &lt;code>/v1/models&lt;/code>.&lt;/strong>&lt;br>
Until &lt;a href="https://github.com/agentgateway/agentgateway/issues/1462">agentgateway#1462&lt;/a> grows a model list, you may get a metadata warning. Ignore it. &lt;code>/v1/responses&lt;/code> still works.&lt;/p>
&lt;p>I always smoke-test the CLI &lt;em>before&lt;/em> I open the pretty chat UI. Saves a lot of &amp;ldquo;is it the bot or the gateway?&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENMAUSBOT_VIRTUAL_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-omb-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">codex --profile agentgateway &lt;span class="s2">&amp;#34;Hello&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You want a &lt;code>200&lt;/code> on &lt;code>/v1/responses&lt;/code> in the agentgateway log (&lt;code>endpoint=api.openai.com:443&lt;/code>, &lt;code>gen_ai.provider.name=openai&lt;/code>). If Codex logs into ChatGPT instead, the profile didn&amp;rsquo;t load. Coffee, then check &lt;code>~/.codex/config.toml&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="talking-to-indigo">Talking to Indigo&lt;/h2>
&lt;p>Desktop builds embed the harness. From source I run both:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/milind-soni/OpenMausBot &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> OpenMausBot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pnpm install
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENMAUSBOT_VIRTUAL_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-omb-...&amp;#39;&lt;/span> &lt;span class="c1"># virtual key only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pnpm dev:server &lt;span class="c1"># 127.0.0.1:8799&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pnpm dev &lt;span class="c1"># http://127.0.0.1:5199&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then, in the browser:&lt;/p>
&lt;ol>
&lt;li>Open &lt;code>http://127.0.0.1:5199&lt;/code>&lt;/li>
&lt;li>Click &lt;strong>Indigo&lt;/strong>&lt;/li>
&lt;li>Model picker → &lt;strong>GPT-5.6 Sol&lt;/strong>&lt;/li>
&lt;li>Type &lt;code>say hi in five words.&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>I picked the categories it offered (Work &amp;amp; projects — I was not in a &amp;ldquo;life admin&amp;rdquo; mood). Claude failed first, which I&amp;rsquo;ll get to. Then Codex came back through the gateway and I grinned at five words.&lt;/p>
&lt;p>&lt;strong>Hi, I&amp;rsquo;m Indigo, your bot.&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/openmausbot-indigo-chat.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-08-15-openmausbot-standalone-agentgateway/openmausbot-indigo-chat.png" alt="OpenMausBot Indigo chat on 127.0.0.1:5199 — Claude wrapper error, then a five-word Codex reply through agentgateway" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="the-claude-face-plant">The Claude face-plant&lt;/h2>
&lt;p>The first bubble in that thread was a red box. I left it in the screenshot on purpose. Saturday labs should be honest.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">claude exited 127 before result: openmausbot-agw wrapper: real claude CLI not installed
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ANTHROPIC_BASE_URL=http://127.0.0.1:4000 (agentgateway)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>127&lt;/code> means &amp;ldquo;I can&amp;rsquo;t find that command.&amp;rdquo; A wrapper was already pointing Claude at agentgateway — the redirect was right — but I hadn&amp;rsquo;t actually installed &lt;code>claude&lt;/code>. OpenMausBot will not invent an Anthropic client because you wished for one.&lt;/p>
&lt;p>If you want that path later, install the CLI and keep &lt;code>ANTHROPIC_API_KEY&lt;/code> on the gateway. The Claude driver deletes it from the child the same way Codex deletes &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># after `claude` is actually installed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_BASE_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;http://127.0.0.1:4000&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I stayed on Codex. Five words were enough for the day.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">Why bother&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Without the gateway&lt;/th>
&lt;th>How Saturday felt with it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>OpenAI key on the laptop, or worse, in the bot&lt;/td>
&lt;td>The key stayed in the agentgateway process. I slept better.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Codex talking to OpenAI with no shared rules&lt;/td>
&lt;td>Virtual key, regex guards, a rate limit I can change without touching Indigo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;What did this cost?&amp;rdquo; shrugged off&lt;/td>
&lt;td>Cost catalog + &lt;code>user: openmausbot&lt;/code> on the turn&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Every new chat app grows another secret&lt;/td>
&lt;td>One &lt;code>:4000&lt;/code> door. Next weekend&amp;rsquo;s toy can use it too.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you already send Claude Code or Codex through agentgateway, this is the same idea with a nicer living room: localhost, &lt;code>/v1&lt;/code>, a bot named Indigo instead of a blinking cursor.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/integrations/llm-clients/codex/">Codex → agentgateway&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/cost-controls/virtual-keys/">Virtual keys&lt;/a>&lt;/li>
&lt;li>Official: &lt;a href="https://agentgateway.dev/docs/standalone/latest/llm/prompt-guards/regex/">Regex guardrails&lt;/a>&lt;/li>
&lt;li>OpenMausBot: &lt;a href="https://github.com/milind-soni/OpenMausBot">milind-soni/OpenMausBot&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">How To: Run agentgateway standalone locally&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">How To: Connect Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/">How To: Point Claude Desktop at agentgateway with Entra SSO&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/">Route MCP / Claude traffic through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">agentgateway standalone cost &amp;amp; tokenomics dashboard&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">Hard spend limits for LLM traffic&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/">First steps: agentgateway and F5 AI Guardrails&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>How To: Point Claude Desktop at agentgateway with Entra SSO</title><link>https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/</link><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-08-12-claude-desktop-entra-agentgateway/</guid><description>&lt;p>Claude Desktop is great until the Anthropic key lives on every laptop and nobody can answer who called which model, when, or for how much.&lt;/p>
&lt;p>The pattern I want is the same one I use for &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code / Codex&lt;/a> and &lt;a href="https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/">MCP through agentgateway&lt;/a>: &lt;strong>SSO at the edge, provider secret in the cluster, one place for policy and cost.&lt;/strong>&lt;/p>
&lt;p>This guide is the Claude Desktop path running in my lab:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Live page: &lt;strong>&lt;a href="https://goose.maniak.ai/claude-desktop.html">goose.maniak.ai/claude-desktop.html&lt;/a>&lt;/strong> · Deep dive: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/docs/claude-desktop-gateway.md">&lt;code>docs/claude-desktop-gateway.md&lt;/code>&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Claude Desktop authenticates with &lt;strong>Entra (PKCE)&lt;/strong>, sends an &lt;strong>ID token&lt;/strong> as Bearer, the gateway validates it &lt;strong>Strict&lt;/strong> against Entra JWKS, then injects the Anthropic key upstream.&lt;/p>
&lt;div class="mermaid">flowchart LR
 CD[Claude Desktop] --&amp;gt;|PKCE Interactive| Entra[Entra ID]
 Entra --&amp;gt;|ID token| CD
 CD --&amp;gt;|Bearer ID token&amp;lt;br/&amp;gt;via 127.0.0.1:18789| GW[claude-desktop-gateway&amp;lt;br/&amp;gt;JWT Strict · entra-jwks]
 GW --&amp;gt; BE[anthropic-claude-desktop&amp;lt;br/&amp;gt;inject anthropic-secret]
 BE --&amp;gt; API[api.anthropic.com&amp;lt;br/&amp;gt;/v1/messages]
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hop&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>1 · PKCE&lt;/strong>&lt;/td>
&lt;td>Entra Interactive public client (&lt;code>agw-claude-desktop&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>2 · Bearer&lt;/strong>&lt;/td>
&lt;td>&lt;strong>ID token&lt;/strong>, not access token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>3 · Gateway&lt;/strong>&lt;/td>
&lt;td>JWT Strict against &lt;code>entra-jwks&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4 · Upstream&lt;/strong>&lt;/td>
&lt;td>Anthropic via &lt;code>anthropic-secret&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Path is &lt;code>/&lt;/code> on this dedicated Gateway so Claude Desktop can call &lt;code>{base}/v1/messages&lt;/code>. The open Anthropic path for kagent stays on &lt;code>anthropic-claude-gateway&lt;/code> (different NodePort / path).&lt;/p>
&lt;hr>
&lt;h2 id="why-a-loopback-proxy">Why a loopback proxy&lt;/h2>
&lt;p>Claude Desktop allows &lt;strong>plain HTTP only on loopback&lt;/strong>. Direct lab HTTPS into Electron often dies with &lt;code>ERR_CERT_AUTHORITY_INVALID&lt;/code> even when browsers can be taught to trust the cert.&lt;/p>
&lt;p>So the recommended lab path is:&lt;/p>
&lt;ol>
&lt;li>Run a tiny TCP proxy on your laptop&lt;/li>
&lt;li>Point Claude Desktop at &lt;code>http://127.0.0.1:18789/&lt;/code>&lt;/li>
&lt;li>Let the proxy forward to the cluster HTTP NodePort&lt;/li>
&lt;/ol>
&lt;p>From &lt;a href="https://github.com/sebbycorp/k8s-goose">k8s-goose&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">python3 scripts/claude-desktop-lab-proxy.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Leave that process running.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Thing&lt;/th>
&lt;th>Lab value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Service&lt;/td>
&lt;td>&lt;code>claude-desktop-gateway&lt;/code> · ns &lt;code>agentgateway-system&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude Desktop base URL&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:18789/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Messages path&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:18789/v1/messages&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Proxy upstream (HTTP NodePort)&lt;/td>
&lt;td>&lt;code>172.16.10.155:31938&lt;/code> · Service &lt;code>80:31938/TCP&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HTTPS NodePort (optional / advanced)&lt;/td>
&lt;td>&lt;code>https://172.16.10.155:31211/&lt;/code> · often rejected by Electron&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Confirm NodePorts anytime:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl --context maniak-goose -n agentgateway-system get svc claude-desktop-gateway -o wide
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Anonymous calls should &lt;strong>401/403&lt;/strong>. A valid Entra ID token should reach Anthropic.&lt;/p>
&lt;hr>
&lt;h2 id="entra-app">Entra app&lt;/h2>
&lt;p>Public client already registered for Claude Desktop’s loopback PKCE flow.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>App name&lt;/td>
&lt;td>&lt;code>agw-claude-desktop&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tenant ID&lt;/td>
&lt;td>&lt;code>8635e970-2205-4189-bc77-77519ff5064f&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client ID&lt;/td>
&lt;td>&lt;code>adf4a4f8-45a4-4bda-a7e2-35f39b1db59d&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Issuer&lt;/td>
&lt;td>&lt;code>https://login.microsoftonline.com/8635e970-2205-4189-bc77-77519ff5064f/v2.0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Platform&lt;/td>
&lt;td>&lt;strong>Mobile and desktop applications&lt;/strong> (not Web)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Redirect URI&lt;/td>
&lt;td>&lt;code>http://127.0.0.1/callback&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Public client flows&lt;/td>
&lt;td>Yes (no client secret · PKCE)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="critical-entra-callouts">Critical Entra callouts&lt;/h3>
&lt;p>&lt;strong>Bearer must be an ID token.&lt;/strong>&lt;br>
Default Claude Desktop mode and this gateway’s &lt;code>aud&lt;/code> = the app’s client ID. Access tokens have a different audience story and fail Strict JWT.&lt;/p>
&lt;p>&lt;strong>Redirect must include &lt;code>/callback&lt;/code>.&lt;/strong>&lt;br>
Register &lt;code>http://127.0.0.1/callback&lt;/code>. &lt;code>http://127.0.0.1&lt;/code> alone fails with &lt;code>AADSTS50011&lt;/code>. The port is wildcarded.&lt;/p>
&lt;p>&lt;strong>Platform = Mobile and desktop — not Web.&lt;/strong>&lt;br>
Entra rejects the loopback PKCE callback on the Web platform. Public client, no secret.&lt;/p>
&lt;p>Include &lt;code>offline_access&lt;/code> so users are not re-prompted every ~1h when using ID tokens.&lt;/p>
&lt;hr>
&lt;h2 id="claude-desktop-developer-config">Claude Desktop developer config&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Help → Troubleshooting → Enable Developer Mode&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Developer → Configure Third-Party Inference…&lt;/strong>&lt;/li>
&lt;li>Fully quit and relaunch after edits if the UI is sticky&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Start the proxy first&lt;/strong>, then set:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Connection / Inference provider&lt;/td>
&lt;td>Gateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway base URL&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:18789/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Credential kind&lt;/td>
&lt;td>Interactive sign-in&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client ID&lt;/td>
&lt;td>&lt;code>adf4a4f8-45a4-4bda-a7e2-35f39b1db59d&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Issuer URL&lt;/td>
&lt;td>&lt;code>https://login.microsoftonline.com/8635e970-2205-4189-bc77-77519ff5064f/v2.0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bearer token&lt;/td>
&lt;td>&lt;strong>ID token&lt;/strong> (not Access token)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scopes&lt;/td>
&lt;td>&lt;code>openid email profile offline_access&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model discovery&lt;/td>
&lt;td>On → &lt;code>http://127.0.0.1:18789/v1/models&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Walkthrough expectation: Entra consent → signed in as your org profile → model discovery fills the picker → a trivial chat (&lt;code>whats 2+2&lt;/code>) comes back through the gateway footer.&lt;/p>
&lt;p>Screenshots from a working lab session live under &lt;a href="https://github.com/sebbycorp/k8s-goose/tree/main/assets/claude-desktop">&lt;code>assets/claude-desktop/&lt;/code>&lt;/a> in k8s-goose.&lt;/p>
&lt;hr>
&lt;h2 id="cluster-resources-gitops">Cluster resources (GitOps)&lt;/h2>
&lt;p>Everything is under &lt;code>config/&lt;/code> via the Argo app &lt;code>agentgateway-config&lt;/code>. Push + sync is enough for the laptop proxy path — no Gateway/JWT YAML changes required just to point Claude Desktop at the lab.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Kind&lt;/th>
&lt;th>Name&lt;/th>
&lt;th>File&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Gateway&lt;/td>
&lt;td>&lt;code>claude-desktop-gateway&lt;/code>&lt;/td>
&lt;td>&lt;code>config/gateway/claude-desktop-gateway.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HTTPRoute&lt;/td>
&lt;td>&lt;code>claude-desktop&lt;/code>&lt;/td>
&lt;td>&lt;code>config/routes/claude-desktop-route.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AgentgatewayBackend&lt;/td>
&lt;td>&lt;code>anthropic-claude-desktop&lt;/code>&lt;/td>
&lt;td>&lt;code>config/backends/anthropic-claude-desktop.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AgentgatewayPolicy&lt;/td>
&lt;td>&lt;code>claude-desktop-jwt-auth&lt;/code>&lt;/td>
&lt;td>&lt;code>config/policies/claude-desktop-jwt-auth.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Lab proxy&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&lt;code>scripts/claude-desktop-lab-proxy.py&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Reused — do not recreate:&lt;/strong> &lt;code>AgentgatewayBackend/entra-jwks&lt;/code>, &lt;code>Secret/anthropic-secret&lt;/code> (ExternalSecret → Vault), &lt;code>Secret/solo-ui-tls&lt;/code> (HTTPS terminate), cost catalog parameters.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">Why bother&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Without gateway&lt;/th>
&lt;th>With this path&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Anthropic key on the laptop&lt;/td>
&lt;td>Key stays in Vault / cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude.ai account or raw API key sprawl&lt;/td>
&lt;td>Org Entra sign-in&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No shared policy / spend story&lt;/td>
&lt;td>Same agentgateway budgets, traces, and cost catalog as the rest of the lab&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you already route Claude Code or MCP through agentgateway, this is the Desktop sibling: same identity story, dedicated Gateway so &lt;code>/v1/messages&lt;/code> just works.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Live lab page: &lt;a href="https://goose.maniak.ai/claude-desktop.html">goose.maniak.ai/claude-desktop.html&lt;/a>&lt;/li>
&lt;li>Markdown deep-dive: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/docs/claude-desktop-gateway.md">docs/claude-desktop-gateway.md&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/">Route MCP / Claude traffic through agentgateway&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>Claude Desktop is great until the Anthropic key lives on every laptop and nobody can answer who called which model, when, or for how much.&lt;/p>
&lt;p>The pattern I want is the same one I use for &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code / Codex&lt;/a> and &lt;a href="https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/">MCP through agentgateway&lt;/a>: &lt;strong>SSO at the edge, provider secret in the cluster, one place for policy and cost.&lt;/strong>&lt;/p>
&lt;p>This guide is the Claude Desktop path running in my lab:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Live page: &lt;strong>&lt;a href="https://goose.maniak.ai/claude-desktop.html">goose.maniak.ai/claude-desktop.html&lt;/a>&lt;/strong> · Deep dive: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/docs/claude-desktop-gateway.md">&lt;code>docs/claude-desktop-gateway.md&lt;/code>&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Claude Desktop authenticates with &lt;strong>Entra (PKCE)&lt;/strong>, sends an &lt;strong>ID token&lt;/strong> as Bearer, the gateway validates it &lt;strong>Strict&lt;/strong> against Entra JWKS, then injects the Anthropic key upstream.&lt;/p>
&lt;div class="mermaid">flowchart LR
 CD[Claude Desktop] --&amp;gt;|PKCE Interactive| Entra[Entra ID]
 Entra --&amp;gt;|ID token| CD
 CD --&amp;gt;|Bearer ID token&amp;lt;br/&amp;gt;via 127.0.0.1:18789| GW[claude-desktop-gateway&amp;lt;br/&amp;gt;JWT Strict · entra-jwks]
 GW --&amp;gt; BE[anthropic-claude-desktop&amp;lt;br/&amp;gt;inject anthropic-secret]
 BE --&amp;gt; API[api.anthropic.com&amp;lt;br/&amp;gt;/v1/messages]
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Hop&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>1 · PKCE&lt;/strong>&lt;/td>
&lt;td>Entra Interactive public client (&lt;code>agw-claude-desktop&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>2 · Bearer&lt;/strong>&lt;/td>
&lt;td>&lt;strong>ID token&lt;/strong>, not access token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>3 · Gateway&lt;/strong>&lt;/td>
&lt;td>JWT Strict against &lt;code>entra-jwks&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4 · Upstream&lt;/strong>&lt;/td>
&lt;td>Anthropic via &lt;code>anthropic-secret&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Path is &lt;code>/&lt;/code> on this dedicated Gateway so Claude Desktop can call &lt;code>{base}/v1/messages&lt;/code>. The open Anthropic path for kagent stays on &lt;code>anthropic-claude-gateway&lt;/code> (different NodePort / path).&lt;/p>
&lt;hr>
&lt;h2 id="why-a-loopback-proxy">Why a loopback proxy&lt;/h2>
&lt;p>Claude Desktop allows &lt;strong>plain HTTP only on loopback&lt;/strong>. Direct lab HTTPS into Electron often dies with &lt;code>ERR_CERT_AUTHORITY_INVALID&lt;/code> even when browsers can be taught to trust the cert.&lt;/p>
&lt;p>So the recommended lab path is:&lt;/p>
&lt;ol>
&lt;li>Run a tiny TCP proxy on your laptop&lt;/li>
&lt;li>Point Claude Desktop at &lt;code>http://127.0.0.1:18789/&lt;/code>&lt;/li>
&lt;li>Let the proxy forward to the cluster HTTP NodePort&lt;/li>
&lt;/ol>
&lt;p>From &lt;a href="https://github.com/sebbycorp/k8s-goose">k8s-goose&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">python3 scripts/claude-desktop-lab-proxy.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Leave that process running.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Thing&lt;/th>
&lt;th>Lab value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Service&lt;/td>
&lt;td>&lt;code>claude-desktop-gateway&lt;/code> · ns &lt;code>agentgateway-system&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude Desktop base URL&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:18789/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Messages path&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:18789/v1/messages&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Proxy upstream (HTTP NodePort)&lt;/td>
&lt;td>&lt;code>172.16.10.155:31938&lt;/code> · Service &lt;code>80:31938/TCP&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HTTPS NodePort (optional / advanced)&lt;/td>
&lt;td>&lt;code>https://172.16.10.155:31211/&lt;/code> · often rejected by Electron&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Confirm NodePorts anytime:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl --context maniak-goose -n agentgateway-system get svc claude-desktop-gateway -o wide
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Anonymous calls should &lt;strong>401/403&lt;/strong>. A valid Entra ID token should reach Anthropic.&lt;/p>
&lt;hr>
&lt;h2 id="entra-app">Entra app&lt;/h2>
&lt;p>Public client already registered for Claude Desktop’s loopback PKCE flow.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>App name&lt;/td>
&lt;td>&lt;code>agw-claude-desktop&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tenant ID&lt;/td>
&lt;td>&lt;code>8635e970-2205-4189-bc77-77519ff5064f&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client ID&lt;/td>
&lt;td>&lt;code>adf4a4f8-45a4-4bda-a7e2-35f39b1db59d&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Issuer&lt;/td>
&lt;td>&lt;code>https://login.microsoftonline.com/8635e970-2205-4189-bc77-77519ff5064f/v2.0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Platform&lt;/td>
&lt;td>&lt;strong>Mobile and desktop applications&lt;/strong> (not Web)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Redirect URI&lt;/td>
&lt;td>&lt;code>http://127.0.0.1/callback&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Public client flows&lt;/td>
&lt;td>Yes (no client secret · PKCE)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="critical-entra-callouts">Critical Entra callouts&lt;/h3>
&lt;p>&lt;strong>Bearer must be an ID token.&lt;/strong>&lt;br>
Default Claude Desktop mode and this gateway’s &lt;code>aud&lt;/code> = the app’s client ID. Access tokens have a different audience story and fail Strict JWT.&lt;/p>
&lt;p>&lt;strong>Redirect must include &lt;code>/callback&lt;/code>.&lt;/strong>&lt;br>
Register &lt;code>http://127.0.0.1/callback&lt;/code>. &lt;code>http://127.0.0.1&lt;/code> alone fails with &lt;code>AADSTS50011&lt;/code>. The port is wildcarded.&lt;/p>
&lt;p>&lt;strong>Platform = Mobile and desktop — not Web.&lt;/strong>&lt;br>
Entra rejects the loopback PKCE callback on the Web platform. Public client, no secret.&lt;/p>
&lt;p>Include &lt;code>offline_access&lt;/code> so users are not re-prompted every ~1h when using ID tokens.&lt;/p>
&lt;hr>
&lt;h2 id="claude-desktop-developer-config">Claude Desktop developer config&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Help → Troubleshooting → Enable Developer Mode&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Developer → Configure Third-Party Inference…&lt;/strong>&lt;/li>
&lt;li>Fully quit and relaunch after edits if the UI is sticky&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Start the proxy first&lt;/strong>, then set:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Connection / Inference provider&lt;/td>
&lt;td>Gateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Gateway base URL&lt;/td>
&lt;td>&lt;code>http://127.0.0.1:18789/&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Credential kind&lt;/td>
&lt;td>Interactive sign-in&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client ID&lt;/td>
&lt;td>&lt;code>adf4a4f8-45a4-4bda-a7e2-35f39b1db59d&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Issuer URL&lt;/td>
&lt;td>&lt;code>https://login.microsoftonline.com/8635e970-2205-4189-bc77-77519ff5064f/v2.0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bearer token&lt;/td>
&lt;td>&lt;strong>ID token&lt;/strong> (not Access token)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scopes&lt;/td>
&lt;td>&lt;code>openid email profile offline_access&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model discovery&lt;/td>
&lt;td>On → &lt;code>http://127.0.0.1:18789/v1/models&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Walkthrough expectation: Entra consent → signed in as your org profile → model discovery fills the picker → a trivial chat (&lt;code>whats 2+2&lt;/code>) comes back through the gateway footer.&lt;/p>
&lt;p>Screenshots from a working lab session live under &lt;a href="https://github.com/sebbycorp/k8s-goose/tree/main/assets/claude-desktop">&lt;code>assets/claude-desktop/&lt;/code>&lt;/a> in k8s-goose.&lt;/p>
&lt;hr>
&lt;h2 id="cluster-resources-gitops">Cluster resources (GitOps)&lt;/h2>
&lt;p>Everything is under &lt;code>config/&lt;/code> via the Argo app &lt;code>agentgateway-config&lt;/code>. Push + sync is enough for the laptop proxy path — no Gateway/JWT YAML changes required just to point Claude Desktop at the lab.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Kind&lt;/th>
&lt;th>Name&lt;/th>
&lt;th>File&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Gateway&lt;/td>
&lt;td>&lt;code>claude-desktop-gateway&lt;/code>&lt;/td>
&lt;td>&lt;code>config/gateway/claude-desktop-gateway.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HTTPRoute&lt;/td>
&lt;td>&lt;code>claude-desktop&lt;/code>&lt;/td>
&lt;td>&lt;code>config/routes/claude-desktop-route.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AgentgatewayBackend&lt;/td>
&lt;td>&lt;code>anthropic-claude-desktop&lt;/code>&lt;/td>
&lt;td>&lt;code>config/backends/anthropic-claude-desktop.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AgentgatewayPolicy&lt;/td>
&lt;td>&lt;code>claude-desktop-jwt-auth&lt;/code>&lt;/td>
&lt;td>&lt;code>config/policies/claude-desktop-jwt-auth.yaml&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Lab proxy&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&lt;code>scripts/claude-desktop-lab-proxy.py&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Reused — do not recreate:&lt;/strong> &lt;code>AgentgatewayBackend/entra-jwks&lt;/code>, &lt;code>Secret/anthropic-secret&lt;/code> (ExternalSecret → Vault), &lt;code>Secret/solo-ui-tls&lt;/code> (HTTPS terminate), cost catalog parameters.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">Why bother&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Without gateway&lt;/th>
&lt;th>With this path&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Anthropic key on the laptop&lt;/td>
&lt;td>Key stays in Vault / cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Claude.ai account or raw API key sprawl&lt;/td>
&lt;td>Org Entra sign-in&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No shared policy / spend story&lt;/td>
&lt;td>Same agentgateway budgets, traces, and cost catalog as the rest of the lab&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you already route Claude Code or MCP through agentgateway, this is the Desktop sibling: same identity story, dedicated Gateway so &lt;code>/v1/messages&lt;/code> just works.&lt;/p>
&lt;hr>
&lt;h2 id="further-reading">Further reading&lt;/h2>
&lt;ul>
&lt;li>Live lab page: &lt;a href="https://goose.maniak.ai/claude-desktop.html">goose.maniak.ai/claude-desktop.html&lt;/a>&lt;/li>
&lt;li>Markdown deep-dive: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/docs/claude-desktop-gateway.md">docs/claude-desktop-gateway.md&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/">Claude Code &amp;amp; Codex through agentgateway&lt;/a>&lt;/li>
&lt;li>Related: &lt;a href="https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/">Route MCP / Claude traffic through agentgateway&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Graph Engineering on Kubernetes: kagent + Agent Substrate</title><link>https://maniak.io/articles/2026-07-31-graph-engineering-kagent-agent-substrate/</link><pubDate>Fri, 31 Jul 2026 10:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-07-31-graph-engineering-kagent-agent-substrate/</guid><description>&lt;h1 id="graph-engineering-on-kubernetes-kagent--agent-substrate">Graph Engineering on Kubernetes: kagent + Agent Substrate&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-31-graph-engineering-kagent-agent-substrate/graph-engineering-hero.jpg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-31-graph-engineering-kagent-agent-substrate/graph-engineering-hero.jpg" alt="Graph engineering on Kubernetes: kagent coordinates agents, tools, and state as a runtime graph on Agent Substrate inside a Kubernetes cluster" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Here&amp;rsquo;s a familiar pattern. You stand up a few agents, each in its own pod. You give one a supervisor prompt that vaguely says &amp;ldquo;delegate when needed.&amp;rdquo; Tools get wired in application code. The demo works.&lt;/p>
&lt;p>Then real traffic shows up. Half the sessions sit idle. Nobody can draw who called whom after an incident. And you&amp;rsquo;re paying for a fleet of pods that spend most of their lives doing nothing.&lt;/p>
&lt;p>That is not a model problem. It is an architecture problem — specifically a &lt;strong>topology&lt;/strong> problem.&lt;/p>
&lt;p>&lt;a href="https://x.com/beamnxw/status/2081022966645535079">A practical guide from beamnxw&lt;/a> cuts through a lot of the fuzzy language. The short version is worth memorizing:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Harness engineering&lt;/strong> builds the machinery around the model.&lt;br>
&lt;strong>Loop engineering&lt;/strong> designs the repeated work-and-feedback cycle.&lt;br>
&lt;strong>Graph engineering&lt;/strong> makes the workflow topology explicit: nodes, branches, joins, state transitions, and controlled cycles.&lt;/p>
&lt;/blockquote>
&lt;p>Even shorter: &lt;strong>environment → feedback → flow&lt;/strong>.&lt;/p>
&lt;p>Those three layers all sit around the same model. They all affect reliability. They can all involve &amp;ldquo;loops.&amp;rdquo; They are still not the same thing. Confusing them is how teams draw twelve-node diagrams on day one, never define what &amp;ldquo;done&amp;rdquo; means, and dump every tool they can find into one agent.&lt;/p>
&lt;p>This post takes that framing seriously — especially the &lt;strong>graph&lt;/strong> layer — and maps it onto open source you can actually run:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://kagent.dev">kagent&lt;/a>&lt;/strong> as the control plane (agents, tools, and edges as Kubernetes CRDs)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/agent-substrate/substrate">Agent Substrate&lt;/a>&lt;/strong> as the execution layer (sandboxed actors that suspend when idle and wake fast)&lt;/li>
&lt;/ul>
&lt;p>The goal is not more jargon. It is a multi-agent system you can review in Git, operate on a cluster, and not overpay for while it waits.&lt;/p>
&lt;hr>
&lt;h2 id="why-the-three-layers-suddenly-matter">Why the three layers suddenly matter&lt;/h2>
&lt;p>A raw language model cannot create files, keep project state, run a test suite, open a browser, enforce an approval rule, or restart a failed job. Those capabilities come from the environment it sits in.&lt;/p>
&lt;p>As agentic software matures, a stack is coming into focus:&lt;/p>
&lt;ol>
&lt;li>At the foundation, the &lt;strong>harness&lt;/strong> — the code and runtime that actually run the model&lt;/li>
&lt;li>Next, &lt;strong>loops&lt;/strong> — repeating execution and quality checks&lt;/li>
&lt;li>Finally, &lt;strong>graphs&lt;/strong> — structured paths that guide the whole process&lt;/li>
&lt;/ol>
&lt;p>Labels are still messy in the wild. &amp;ldquo;Agent harness&amp;rdquo; is starting to mean something specific. &amp;ldquo;Loop engineering&amp;rdquo; is newer practitioner language. &lt;strong>Graph engineering&lt;/strong> here is practical, not academic: you are building agent workflows as explicit directed graphs or state machines — control flow and state transitions, &lt;em>not&lt;/em> knowledge-graph entity modeling.&lt;/p>
&lt;p>That distinction matters the moment an agent leaves a demo notebook and starts touching files, APIs, customers, or production clusters.&lt;/p>
&lt;hr>
&lt;h2 id="layer-1--harness-the-environment">Layer 1 — Harness: the environment&lt;/h2>
&lt;p>The harness is everything outside the weights. In practice that usually includes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Context injection&lt;/strong> — instructions, retrieved facts, conversation state, task policies&lt;/li>
&lt;li>&lt;strong>Action surfaces&lt;/strong> — APIs, shells, browsers, databases, MCP tools&lt;/li>
&lt;li>&lt;strong>Persistence&lt;/strong> — files, checkpoints, sessions, progress logs, memory&lt;/li>
&lt;li>&lt;strong>Execution control&lt;/strong> — timeouts, retries, budgets, model routing, sub-agent spawning, approval gates&lt;/li>
&lt;li>&lt;strong>Safety&lt;/strong> — permissions, isolation, allow lists, secret handling&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — traces, tool I/O, cost, latency, eval results&lt;/li>
&lt;/ul>
&lt;p>A useful trick from the same guide: &lt;em>remove the model from your architecture diagram&lt;/em>. What is left is probably the harness.&lt;/p>
&lt;p>Two teams can use the same foundation model and get very different outcomes because one gives the model clean tools, a stable workspace, constrained permissions, and observable state — and the other gives it a vague prompt and an unreliable API wrapper. The intelligence may be similar. The working conditions are not.&lt;/p>
&lt;p>On our stack, a large part of the harness is &lt;strong>Agent Substrate&lt;/strong>: gVisor actors, WorkerPools, snapshot/restore to object storage, network isolation, and the lifecycle that makes idle sessions cheap. Tools and memory still come through kagent and MCP. The point is the same as the article&amp;rsquo;s: &lt;strong>the harness is the working system&lt;/strong>, not a footnote under the model card.&lt;/p>
&lt;hr>
&lt;h2 id="layer-2--loop-evidence-and-stop-rules">Layer 2 — Loop: evidence and stop rules&lt;/h2>
&lt;p>Every tool-using agent already has a small loop:&lt;/p>
&lt;p>call the model → look at the result → run tools → feed observations back → repeat until something terminal happens.&lt;/p>
&lt;p>&lt;strong>Loop engineering&lt;/strong> starts when you deliberately design cycles &lt;em>around&lt;/em> that behavior: verification loops, event-driven wakeups, improvement loops that learn from traces. LangChain&amp;rsquo;s newer framing treats these as a &lt;em>stack&lt;/em> of loops, not one magic &lt;code>while&lt;/code>.&lt;/p>
&lt;p>A well-engineered loop has anatomy:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Trigger&lt;/strong> — user request, schedule, failed test, new data, grader feedback&lt;/li>
&lt;li>&lt;strong>Goal&lt;/strong> — a specific state to reach, not &amp;ldquo;keep improving&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>State and memory&lt;/strong> — what the next cycle needs without replaying everything&lt;/li>
&lt;li>&lt;strong>Action policy&lt;/strong> — what the agent may change, call, delegate, or spend&lt;/li>
&lt;li>&lt;strong>Evidence&lt;/strong> — tests, schema validation, citations, diffs, metrics, human review&lt;/li>
&lt;li>&lt;strong>Feedback&lt;/strong> — compact, actionable description of why evidence failed&lt;/li>
&lt;li>&lt;strong>Stopping rule&lt;/strong> — success, budget, timeout, irrecoverable error, human escalation&lt;/li>
&lt;/ul>
&lt;p>The key line from beamnxw, which I keep repeating to teams:&lt;/p>
&lt;blockquote>
&lt;p>Do not loop on confidence. Loop on evidence.&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;The agent says it is done&amp;rdquo; is not a stopping condition. &amp;ldquo;The tests pass, the links resolve, the schema validates, and the reviewer approves&amp;rdquo; is.&lt;/p>
&lt;p>A prompt tells the model what to do during a call. A &lt;strong>loop&lt;/strong> specifies what the system does &lt;em>after&lt;/em> the call: how it observes results, chooses feedback, decides whether to continue, persists progress, and terminates. Prompt quality still matters. The loop turns a one-shot instruction into a managed process.&lt;/p>
&lt;p>Tradeoff: every grader, reviewer, or retry costs latency and money. Prefer the simplest architecture that works. Add loops where the cost of failure is higher than the cost of verification.&lt;/p>
&lt;p>On kagent, those loops live &lt;strong>inside&lt;/strong> each graph node — the research agent, the writer, the reviewer — not as a vague global &amp;ldquo;try harder&amp;rdquo; policy.&lt;/p>
&lt;hr>
&lt;h2 id="layer-3--graph-engineering-the-deep-dive">Layer 3 — Graph engineering (the deep dive)&lt;/h2>
&lt;p>This is the heart of the post. Graph engineering asks a different question than harness or loop:&lt;/p>
&lt;blockquote>
&lt;p>Not only &lt;em>what&lt;/em> the agent does — &lt;em>what component is permitted to run next?&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>Steps are &lt;strong>nodes&lt;/strong>. Allowed next steps are &lt;strong>edges&lt;/strong>. Edges can express sequence, conditional branching, parallel fan-out, joins, loops, and human interrupts. &lt;strong>State traverses the graph.&lt;/strong> The topology is what makes control flow checkable — by you, by a reviewer, by Git.&lt;/p>
&lt;p>LangGraph-style systems emphasize durable execution, shared state, and human-in-the-loop control. AutoGen&amp;rsquo;s docs put it bluntly: use a graph when you need exact control over agent order, different next steps for different outcomes, deterministic branching, or complex multi-step processes with cycles.&lt;/p>
&lt;p>That is not &amp;ldquo;draw boxes because multi-agent is trendy.&amp;rdquo; That is &lt;strong>control over agents&lt;/strong>, not another abstraction for the sake of it.&lt;/p>
&lt;h3 id="what-graph-engineers-actually-decide">What graph engineers actually decide&lt;/h3>
&lt;p>beamnxw lists the real design work. This is the checklist I use when someone says &amp;ldquo;we should make it multi-agent&amp;rdquo;:&lt;/p>
&lt;p>&lt;strong>1. Node boundaries&lt;/strong>&lt;br>
Which work belongs in a deterministic function, an LLM call, a specialist agent, or a human review step?&lt;br>
If everything is &amp;ldquo;another LLM agent,&amp;rdquo; you have not drawn boundaries — you have multiplied prompts.&lt;/p>
&lt;p>&lt;strong>2. State schema&lt;/strong>&lt;br>
What may each node read or update? How do parallel updates merge?&lt;br>
If state is &amp;ldquo;whatever is in the chat log,&amp;rdquo; joins and recovery will hurt.&lt;/p>
&lt;p>&lt;strong>3. Routing conditions&lt;/strong>&lt;br>
Which &lt;em>evidence&lt;/em> sends work forward, backward, sideways, or to escalation?&lt;br>
Routing on vibes (&amp;ldquo;looks good&amp;rdquo;) is how you reintroduce invisible control flow.&lt;/p>
&lt;p>&lt;strong>4. Concurrency&lt;/strong>&lt;br>
What can run in parallel? What must join? What shared resources need coordination?&lt;br>
Fan-out without a join plan is just concurrent chaos.&lt;/p>
&lt;p>&lt;strong>5. Cycles and exits&lt;/strong>&lt;br>
Where are retries legal? How many are allowed? What makes the cycle &lt;em>safe&lt;/em>?&lt;br>
A cycle without an exit is a cost leak with extra arrows.&lt;/p>
&lt;p>&lt;strong>6. Durability&lt;/strong>&lt;br>
Where do checkpoints occur? How does execution resume after interruption?&lt;br>
Long-running multi-agent work without durability is a demo, not a system.&lt;/p>
&lt;p>If you cannot answer those six, you do not have a graph design yet. You have a slide.&lt;/p>
&lt;h3 id="when-a-graph-is-worth-the-ceremony">When a graph is worth the ceremony&lt;/h3>
&lt;p>Graphs earn their keep when the process has:&lt;/p>
&lt;ul>
&lt;li>meaningful &lt;strong>branches&lt;/strong>&lt;/li>
&lt;li>&lt;strong>parallel&lt;/strong> work that must rejoin&lt;/li>
&lt;li>&lt;strong>approvals&lt;/strong>&lt;/li>
&lt;li>&lt;strong>recovery&lt;/strong> paths&lt;/li>
&lt;li>multiple &lt;strong>specialist&lt;/strong> agents with different tools and prompts&lt;/li>
&lt;/ul>
&lt;p>They are &lt;em>less&lt;/em> useful when the job is simply &amp;ldquo;give one agent three tools and let it work.&amp;rdquo;&lt;/p>
&lt;p>There is also a real risk the article calls out: &lt;strong>a graph can freeze assumptions too early&lt;/strong>. If the model must dynamically invent the plan, forcing every possible path into a diagram can make the system more brittle, not less. Formalize paths you have observed. Do not invent a twenty-node topology on day one because the architecture blog had pretty boxes.&lt;/p>
&lt;h3 id="how-the-three-layers-nest-in-one-system">How the three layers nest in one system&lt;/h3>
&lt;p>Notice the nesting carefully:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>graph&lt;/strong> runs with support from the harness&lt;/li>
&lt;li>One or more &lt;strong>loops&lt;/strong> live &lt;em>inside&lt;/em> the graph (usually inside nodes)&lt;/li>
&lt;li>The &lt;strong>harness&lt;/strong> supplies the state, tools, sandboxes, and evaluators those loops need&lt;/li>
&lt;/ul>
&lt;p>Categories overlap because software layers overlap. Each still gives you a different lever when the system fails:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If you see…&lt;/th>
&lt;th>Fix this layer first&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cannot resume, loses workspace, tool surface is a mess, permissions are wrong&lt;/td>
&lt;td>&lt;strong>Harness&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Runs forever, no proof of success, retries have no budget&lt;/td>
&lt;td>&lt;strong>Loop&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wrong specialist runs, no approval path, unreadable flow, no recovery route&lt;/td>
&lt;td>&lt;strong>Graph&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Do not blame the model for orchestration failures. A model cannot compensate reliably for stale state, ambiguous tool schemas, broken APIs, or missing exit conditions. Improve the layer that owns the failure.&lt;/p>
&lt;h3 id="expensive-mistakes-steal-these-warnings">Expensive mistakes (steal these warnings)&lt;/h3>
&lt;p>These come almost straight from the same guide, and I have watched all of them in real projects:&lt;/p>
&lt;p>&lt;strong>Building a graph before understanding the work.&lt;/strong>&lt;br>
Teams translate a business process into dozens of nodes before they have watched a capable agent solve it. Start with traces from a simpler harness. Formalize the stable paths.&lt;/p>
&lt;p>&lt;strong>Letting the same model write and grade without safeguards.&lt;/strong>&lt;br>
Self-review can help, but it shares blind spots. Prefer deterministic checks where possible, separate reviewer context, and require humans for high-impact actions.&lt;/p>
&lt;p>&lt;strong>Using &amp;ldquo;keep trying&amp;rdquo; as a loop specification.&lt;/strong>&lt;br>
Unbounded retry is a cost leak. Every loop needs a measurable objective, fresh evidence, max attempts, and a named escalation path.&lt;/p>
&lt;p>&lt;strong>Treating the harness as a dumping ground.&lt;/strong>&lt;br>
More tools and memory are not automatically better. Crowded toolsets raise selection errors. Noisy context raises confusion. Broad permissions raise risk.&lt;/p>
&lt;p>&lt;strong>Blaming the model for orchestration failures.&lt;/strong>&lt;br>
If the graph has no exits and the harness loses state, a better model will not save you.&lt;/p>
&lt;hr>
&lt;h2 id="mapping-graph-engineering-onto-kagent--substrate">Mapping graph engineering onto kagent + substrate&lt;/h2>
&lt;p>Now the practical part: how do those abstract graph decisions show up as Kubernetes resources?&lt;/p>
&lt;h3 id="the-split-of-ownership">The split of ownership&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> You / UI / Git / CronJob
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kagent — graph control plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (nodes, edges, models, MCP, HITL)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ create / resume actor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Agent Substrate — harness density
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (WorkerPool, gVisor, snapshots)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Running actors
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (loops inside each node session)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>kagent&lt;/strong> owns topology: which agents exist, what they may call, which model they use, when a human must approve&lt;/li>
&lt;li>&lt;strong>Substrate&lt;/strong> owns dense, isolated execution: suspend idle sessions, restore sub-second, sandbox with gVisor&lt;/li>
&lt;li>&lt;strong>Kubernetes&lt;/strong> owns platform primitives: CRDs, CronJobs, GitOps, RBAC&lt;/li>
&lt;/ul>
&lt;p>Put simply:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>kagent makes the graph declarative. Substrate makes idle nodes cheap and isolated. Loops still live inside the nodes.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;h3 id="graph-decisions--concrete-objects">Graph decisions → concrete objects&lt;/h3>
&lt;p>Here is beamnxw&amp;rsquo;s design list, translated into this stack:&lt;/p>
&lt;p>&lt;strong>Node boundaries&lt;/strong>&lt;br>
A specialist is a &lt;code>SandboxAgent&lt;/code> (declarative Go ADK on substrate) or an &lt;code>AgentHarness&lt;/code> (coding backends like OpenClaw/Hermes). A deterministic step might stay outside the LLM entirely. A human step is &lt;code>requireApproval&lt;/code> on a tool — or a separate operator-facing path. Do not make every box an LLM if a function or a human is the right node type.&lt;/p>
&lt;p>&lt;strong>State schema&lt;/strong>&lt;br>
Prefer explicit memory, prompt templates, and session/ACP state over &amp;ldquo;the whole chat forever.&amp;rdquo; Each node should know what it is allowed to read and what it is allowed to write. Parallel specialists need a merge story (orchestrator collects structured outputs; do not hope free-form prose merges cleanly).&lt;/p>
&lt;p>&lt;strong>Routing conditions&lt;/strong>&lt;br>
In a declarative multi-agent setup on kagent, routing often lives in the orchestrator&amp;rsquo;s system message &lt;em>plus&lt;/em> the hard constraint of which agents appear as tools. That is weaker than a pure state-machine framework for complex branching — and that is fine if your edges are few and stable. For high-stakes branching, make the condition evidence-based in the prompt &lt;em>and&lt;/em> keep the legal next agents limited to the tool list. Illegal edges simply do not exist.&lt;/p>
&lt;p>&lt;strong>Concurrency&lt;/strong>&lt;br>
If two specialists can run without shared mutable state, you can fan out (sequentially via orchestrator today, or with explicit parallel patterns as your stack grows). Size the &lt;strong>WorkerPool&lt;/strong> for peak concurrent &lt;em>running&lt;/em> actors, not for total nodes in the graph.&lt;/p>
&lt;p>&lt;strong>Cycles and exits&lt;/strong>&lt;br>
A research ↔ revise cycle is an edge back to writer/reviewer with a max iteration count in the orchestrator policy. Without a max and an escalation edge (human), you reinvent unbounded &amp;ldquo;keep trying.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Durability&lt;/strong>&lt;br>
This is where substrate shines. Idle graph nodes do not need to hold pods. Actors checkpoint RAM + filesystem to object storage and resume when the next edge fires. Graph durability is &amp;ldquo;the session can survive interruption&amp;rdquo;; Kubernetes CronJobs handle &amp;ldquo;start this path on a schedule.&amp;rdquo;&lt;/p>
&lt;h3 id="edges-you-can-review-in-git">Edges you can review in Git&lt;/h3>
&lt;p>In kagent, an edge is usually one of:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Agent-as-tool&lt;/strong> — orchestrator may call &lt;code>research-node&lt;/code>&lt;/li>
&lt;li>&lt;strong>MCP tool binding&lt;/strong> — node may call a named tool on a ToolServer&lt;/li>
&lt;li>&lt;strong>HITL gate&lt;/strong> — tool requires human approval before it runs&lt;/li>
&lt;/ol>
&lt;p>That is the difference between &amp;ldquo;the supervisor will figure it out&amp;rdquo; and &amp;ldquo;the allowed topology is in the CRD.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h2 id="building-the-graph-on-this-stack">Building the graph on this stack&lt;/h2>
&lt;h3 id="install-order-harness-under-the-graph">Install order (harness under the graph)&lt;/h3>
&lt;p>Install &lt;strong>Agent Substrate&lt;/strong> first (&lt;code>ate-system&lt;/code>), then &lt;strong>kagent&lt;/strong> with substrate enabled, then a &lt;strong>WorkerPool&lt;/strong>, then &lt;code>ModelConfig&lt;/code>, then the agents that form your graph.&lt;/p>
&lt;p>I will not re-paste every Helm flag — see the &lt;a href="https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/">kind guide&lt;/a> and &lt;a href="https://maniak.io/articles/2026-07-13-kagent-oss-agent-substrate-kind-guide/">code share&lt;/a>. Constraint worth knowing early: substrate-backed declarative agents want the &lt;strong>Go&lt;/strong> runtime today.&lt;/p>
&lt;h3 id="declare-nodes-as-sandboxagents">Declare nodes as SandboxAgents&lt;/h3>
&lt;p>A node is an honest job description: system message, model, tools, pool.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SandboxAgent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">research-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Research specialist — structured facts only&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You research a topic and return structured facts only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Prefer tools over guesswork. Cite sources when you can.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Stop when the evidence is enough — do not invent certainty.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">substrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">substrate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workerPoolRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Writer and reviewer nodes are the same shape with different prompts and tool allowlists. That &lt;em>is&lt;/em> node boundary design.&lt;/p>
&lt;h3 id="draw-edges-with-agent-as-tool">Draw edges with agent-as-tool&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SandboxAgent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">briefing-orchestrator&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Research → draft → review for an industry briefing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You own the briefing pipeline.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1) Call research-node. Require structured facts with sources.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2) Call writer-node with those facts only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3) Call reviewer-node. If evidence fails, send feedback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> to writer-node at most twice, then escalate to a human.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Never invent a specialist that is not in your tool list.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">research-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">writer-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reviewer-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">substrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">substrate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workerPoolRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read that system message again as &lt;strong>graph policy&lt;/strong>: sequence, evidence-based cycle with a max, escalation exit, and a hard tool list so illegal edges do not exist. That is loop design living inside a graph node — exactly the nesting the three-layer model describes.&lt;/p>
&lt;h3 id="human-interrupts-as-real-nodes">Human interrupts as real nodes&lt;/h3>
&lt;p>High-impact tools should not be free edges. With kagent, &lt;code>requireApproval&lt;/code> pauses execution until a person decides. That is a human interrupt in graph terms — see &lt;a href="https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/">HITL on kagent&lt;/a>. Prefer it for publish, delete, deploy, and spend. Leave pure reads free so the graph does not drown in clicks.&lt;/p>
&lt;h3 id="schedules-cronjob-as-the-missing-trigger">Schedules: CronJob as the missing trigger&lt;/h3>
&lt;p>Neither kagent nor substrate is primarily a calendar product. Kubernetes already is.&lt;/p>
&lt;p>Keep nodes as CRDs (mostly idle, cheap on substrate). Use a &lt;code>CronJob&lt;/code> to fire the orchestrator on a schedule. Substrate resumes the actor, the graph runs, the actor suspends again.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">batch/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CronJob&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">daily-briefing-trigger&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0 8 * * *&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jobTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restartPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OnFailure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">trigger&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">curlimages/curl:8.5.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">sS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">X&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">http://kagent-controller.kagent.svc:8083/api/a2a/kagent/briefing-orchestrator/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Adjust path, body, and auth for your environment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Trigger from CronJob. Topology from CRDs. Density from substrate. No fourth control plane for &amp;ldquo;08:00.&amp;rdquo;&lt;/p>
&lt;h3 id="why-sparse-graphs-stay-cheap">Why sparse graphs stay cheap&lt;/h3>
&lt;p>Most multi-agent graphs are sparse in &lt;em>time&lt;/em>. Research runs, then sleeps. Writer runs, then sleeps. Reviewer runs for a moment. Orchestrator waits on a human.&lt;/p>
&lt;p>Pod-per-agent pays for the worst case continuously. Substrate pays for concurrent &lt;strong>running&lt;/strong> work:&lt;/p>
&lt;ol>
&lt;li>Edge fires / request arrives&lt;/li>
&lt;li>Actor restores onto a free WorkerPool pod&lt;/li>
&lt;li>Node loop runs inside gVisor&lt;/li>
&lt;li>Idle → checkpoint to object storage → worker returns to the pool&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale workerpool kagent-default -n kagent --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">5&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Scale concurrency, not &amp;ldquo;number of boxes in the architecture diagram.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-research-and-publish-briefing">Worked example: research-and-publish briefing&lt;/h2>
&lt;p>beamnxw uses a research-and-publishing agent as the running example of how layers nest. Here is the same story on kagent + substrate.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">CronJob&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="n">briefing&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">orchestrator&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">├─▶&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">research&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="err">──&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">structured&lt;/span> &lt;span class="n">facts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">├─▶&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="err">──&lt;/span> &lt;span class="n">draft&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="n">facts&lt;/span> &lt;span class="n">only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└─▶&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">reviewer&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="err">──&lt;/span> &lt;span class="n">evidence&lt;/span> &lt;span class="n">checks&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">pass&lt;/span>&lt;span class="err">?&lt;/span> &lt;span class="err">─┼─&lt;/span> &lt;span class="n">yes&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">publish&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">optional&lt;/span> &lt;span class="n">HITL&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└─&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">feedback&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">writer&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">max&lt;/span> &lt;span class="n">N&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">then&lt;/span> &lt;span class="n">escalate&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">human&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Graph decisions in this design:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nodes:&lt;/strong> orchestrator, research, writer, reviewer, human (on publish)&lt;/li>
&lt;li>&lt;strong>Edges:&lt;/strong> agent-as-tool refs; optional cycle writer ↔ reviewer; escalate edge&lt;/li>
&lt;li>&lt;strong>State:&lt;/strong> structured facts object, draft, review findings — not one blob of chat&lt;/li>
&lt;li>&lt;strong>Routing:&lt;/strong> reviewer evidence decides pass / revise / escalate&lt;/li>
&lt;li>&lt;strong>Cycles:&lt;/strong> max two revise loops, then human&lt;/li>
&lt;li>&lt;strong>Durability:&lt;/strong> each specialist session is a substrate actor; idle does not hold a pod&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Loop decisions:&lt;/strong> mostly inside reviewer — grade against sources, return compact feedback, stop on pass, budget, or escalate.&lt;/p>
&lt;p>&lt;strong>Harness decisions:&lt;/strong> gVisor isolation, tight MCP tool lists per node, OTel traces, snapshots so a long briefing can pause on HITL without burning a warm Deployment overnight.&lt;/p>
&lt;p>If you only remember one sequencing rule from the article: &lt;strong>do not draw this whole graph on day one.&lt;/strong> Start with a single agent and traces. Promote stable handoffs into CRDs when you see them repeating.&lt;/p>
&lt;hr>
&lt;h2 id="questions-worth-asking-before-you-scale-this">Questions worth asking before you scale this&lt;/h2>
&lt;p>Before you grow the graph, sit with a few questions. They are adapted from the production checklist in the beamnxw guide — translated into this stack.&lt;/p>
&lt;p>&lt;strong>On the graph:&lt;/strong> Which paths must be deterministic? Where can work run in parallel? What state is shared, and who merges it? Where are the human gates and recovery routes? Could a teammate see the legal topology from the YAML alone?&lt;/p>
&lt;p>&lt;strong>On the loops:&lt;/strong> What evidence proves success for each node? What feedback comes back on failure? How many retries? What happens when the budget is gone?&lt;/p>
&lt;p>&lt;strong>On the harness:&lt;/strong> Are tools few, clear, and observable? Does state survive suspend/resume? Are permissions tight? Can someone pause, inspect, and resume a run without redeploying the world?&lt;/p>
&lt;p>&lt;strong>On ops:&lt;/strong> Can you replay a bad run and point at a CRD change that fixed it? Are you watching cost, latency, failure rate, and how often a human has to step in?&lt;/p>
&lt;p>If you cannot answer the graph questions, a bigger model will not save you. If you cannot answer the harness questions, more nodes will not either.&lt;/p>
&lt;hr>
&lt;h2 id="when-you-should-not-build-a-graph">When you should &lt;em>not&lt;/em> build a graph&lt;/h2>
&lt;p>If the job is &amp;ldquo;one agent, three tools, let it work,&amp;rdquo; you do not need twelve nodes. Build a solid harness and a tight loop first.&lt;/p>
&lt;p>Add a multi-node graph when you keep reimplementing the same branches, specialist handoffs, recovery paths, or human gates in glue code — and when those paths are stable enough that freezing them is a feature, not a trap.&lt;/p>
&lt;p>Graph engineering is for control you can inspect. It is not a fashion statement.&lt;/p>
&lt;hr>
&lt;h2 id="bottom-line">Bottom line&lt;/h2>
&lt;p>Steal the three-layer framing and use it on purpose:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Harness&lt;/strong> — the environment that makes the model operable&lt;/li>
&lt;li>&lt;strong>Loop&lt;/strong> — the iterative process with evidence and stop rules&lt;/li>
&lt;li>&lt;strong>Graph&lt;/strong> — the explicit map of what may run next&lt;/li>
&lt;/ul>
&lt;p>None of them replaces the others. A pretty graph with a broken harness still loses state. A great harness with no stop rules still burns money. Careful loops still turn to spaghetti when branching and approvals live only in ad-hoc code.&lt;/p>
&lt;p>On Kubernetes, the split is clean enough to ship:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>kagent&lt;/strong> makes the graph (and a lot of the policy) declarative&lt;/li>
&lt;li>&lt;strong>Agent Substrate&lt;/strong> makes idle nodes cheap and isolated&lt;/li>
&lt;li>&lt;strong>You&lt;/strong> still design node boundaries, evidence, and exits&lt;/li>
&lt;li>&lt;strong>CronJobs&lt;/strong> handle &amp;ldquo;run this path at 8am&amp;rdquo; without inventing a new product&lt;/li>
&lt;/ul>
&lt;p>Keep the topology in Git. Scale the WorkerPool for concurrent work, not for diagram vanity. Put humans on tools that can hurt.&lt;/p>
&lt;hr>
&lt;h2 id="where-to-start-call-to-action">Where to start (call to action)&lt;/h2>
&lt;p>You do not need the full briefing pipeline on day one. Do this instead:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Stand up the harness.&lt;/strong> Install Agent Substrate, then kagent with substrate enabled, then a small WorkerPool. Follow the hands-on path:&lt;br>
&lt;a href="https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/">Suspend &amp;amp; resume on kind&lt;/a> · &lt;a href="https://maniak.io/articles/2026-07-13-kagent-oss-agent-substrate-kind-guide/">OSS substrate code share&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Ship one node you trust.&lt;/strong> One &lt;code>SandboxAgent&lt;/code>, one model, a short tool list. Chat with it. Watch it suspend and resume. Get comfortable before you draw edges.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Add edges only when traces demand them.&lt;/strong> Promote repeated handoffs into agent-as-tool refs. Keep illegal specialists off the tool list so they cannot run by accident.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Put a human on the dangerous tools.&lt;/strong> Start with &lt;a href="https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/">HITL on kagent&lt;/a>. Approve deletes and deploys. Leave reads free.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Put the YAML in Git.&lt;/strong> Treat prompt, model, and tool changes like production config. &lt;a href="https://maniak.io/articles/2026-04-02-gitops-for-agents-deployment-and-management/">GitOps for agents&lt;/a> is the longer version of that argument.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Schedule later.&lt;/strong> When you need calendar wakeups, add a CronJob that triggers the orchestrator. Do not wait for a native &amp;ldquo;agent cron&amp;rdquo; product.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Runnable demos live in &lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">&lt;code>sebbycorp/kagent-demos&lt;/code>&lt;/a>. Upstream docs: &lt;a href="https://kagent.dev">kagent.dev&lt;/a> · &lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a> · &lt;a href="https://github.com/kagent-dev/kagent">github.com/kagent-dev/kagent&lt;/a>. The conceptual spine for this post is still &lt;a href="https://x.com/beamnxw/status/2081022966645535079">beamnxw&amp;rsquo;s three-layer guide&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Your move:&lt;/strong> clone the demo, run one substrate-backed agent on kind this week, and only then decide which edges belong in the graph. Topology you have not earned from traces is just decoration.&lt;/p></description><content:encoded>&lt;h1 id="graph-engineering-on-kubernetes-kagent--agent-substrate">Graph Engineering on Kubernetes: kagent + Agent Substrate&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-31-graph-engineering-kagent-agent-substrate/graph-engineering-hero.jpg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-31-graph-engineering-kagent-agent-substrate/graph-engineering-hero.jpg" alt="Graph engineering on Kubernetes: kagent coordinates agents, tools, and state as a runtime graph on Agent Substrate inside a Kubernetes cluster" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Here&amp;rsquo;s a familiar pattern. You stand up a few agents, each in its own pod. You give one a supervisor prompt that vaguely says &amp;ldquo;delegate when needed.&amp;rdquo; Tools get wired in application code. The demo works.&lt;/p>
&lt;p>Then real traffic shows up. Half the sessions sit idle. Nobody can draw who called whom after an incident. And you&amp;rsquo;re paying for a fleet of pods that spend most of their lives doing nothing.&lt;/p>
&lt;p>That is not a model problem. It is an architecture problem — specifically a &lt;strong>topology&lt;/strong> problem.&lt;/p>
&lt;p>&lt;a href="https://x.com/beamnxw/status/2081022966645535079">A practical guide from beamnxw&lt;/a> cuts through a lot of the fuzzy language. The short version is worth memorizing:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Harness engineering&lt;/strong> builds the machinery around the model.&lt;br>
&lt;strong>Loop engineering&lt;/strong> designs the repeated work-and-feedback cycle.&lt;br>
&lt;strong>Graph engineering&lt;/strong> makes the workflow topology explicit: nodes, branches, joins, state transitions, and controlled cycles.&lt;/p>
&lt;/blockquote>
&lt;p>Even shorter: &lt;strong>environment → feedback → flow&lt;/strong>.&lt;/p>
&lt;p>Those three layers all sit around the same model. They all affect reliability. They can all involve &amp;ldquo;loops.&amp;rdquo; They are still not the same thing. Confusing them is how teams draw twelve-node diagrams on day one, never define what &amp;ldquo;done&amp;rdquo; means, and dump every tool they can find into one agent.&lt;/p>
&lt;p>This post takes that framing seriously — especially the &lt;strong>graph&lt;/strong> layer — and maps it onto open source you can actually run:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://kagent.dev">kagent&lt;/a>&lt;/strong> as the control plane (agents, tools, and edges as Kubernetes CRDs)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://github.com/agent-substrate/substrate">Agent Substrate&lt;/a>&lt;/strong> as the execution layer (sandboxed actors that suspend when idle and wake fast)&lt;/li>
&lt;/ul>
&lt;p>The goal is not more jargon. It is a multi-agent system you can review in Git, operate on a cluster, and not overpay for while it waits.&lt;/p>
&lt;hr>
&lt;h2 id="why-the-three-layers-suddenly-matter">Why the three layers suddenly matter&lt;/h2>
&lt;p>A raw language model cannot create files, keep project state, run a test suite, open a browser, enforce an approval rule, or restart a failed job. Those capabilities come from the environment it sits in.&lt;/p>
&lt;p>As agentic software matures, a stack is coming into focus:&lt;/p>
&lt;ol>
&lt;li>At the foundation, the &lt;strong>harness&lt;/strong> — the code and runtime that actually run the model&lt;/li>
&lt;li>Next, &lt;strong>loops&lt;/strong> — repeating execution and quality checks&lt;/li>
&lt;li>Finally, &lt;strong>graphs&lt;/strong> — structured paths that guide the whole process&lt;/li>
&lt;/ol>
&lt;p>Labels are still messy in the wild. &amp;ldquo;Agent harness&amp;rdquo; is starting to mean something specific. &amp;ldquo;Loop engineering&amp;rdquo; is newer practitioner language. &lt;strong>Graph engineering&lt;/strong> here is practical, not academic: you are building agent workflows as explicit directed graphs or state machines — control flow and state transitions, &lt;em>not&lt;/em> knowledge-graph entity modeling.&lt;/p>
&lt;p>That distinction matters the moment an agent leaves a demo notebook and starts touching files, APIs, customers, or production clusters.&lt;/p>
&lt;hr>
&lt;h2 id="layer-1--harness-the-environment">Layer 1 — Harness: the environment&lt;/h2>
&lt;p>The harness is everything outside the weights. In practice that usually includes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Context injection&lt;/strong> — instructions, retrieved facts, conversation state, task policies&lt;/li>
&lt;li>&lt;strong>Action surfaces&lt;/strong> — APIs, shells, browsers, databases, MCP tools&lt;/li>
&lt;li>&lt;strong>Persistence&lt;/strong> — files, checkpoints, sessions, progress logs, memory&lt;/li>
&lt;li>&lt;strong>Execution control&lt;/strong> — timeouts, retries, budgets, model routing, sub-agent spawning, approval gates&lt;/li>
&lt;li>&lt;strong>Safety&lt;/strong> — permissions, isolation, allow lists, secret handling&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — traces, tool I/O, cost, latency, eval results&lt;/li>
&lt;/ul>
&lt;p>A useful trick from the same guide: &lt;em>remove the model from your architecture diagram&lt;/em>. What is left is probably the harness.&lt;/p>
&lt;p>Two teams can use the same foundation model and get very different outcomes because one gives the model clean tools, a stable workspace, constrained permissions, and observable state — and the other gives it a vague prompt and an unreliable API wrapper. The intelligence may be similar. The working conditions are not.&lt;/p>
&lt;p>On our stack, a large part of the harness is &lt;strong>Agent Substrate&lt;/strong>: gVisor actors, WorkerPools, snapshot/restore to object storage, network isolation, and the lifecycle that makes idle sessions cheap. Tools and memory still come through kagent and MCP. The point is the same as the article&amp;rsquo;s: &lt;strong>the harness is the working system&lt;/strong>, not a footnote under the model card.&lt;/p>
&lt;hr>
&lt;h2 id="layer-2--loop-evidence-and-stop-rules">Layer 2 — Loop: evidence and stop rules&lt;/h2>
&lt;p>Every tool-using agent already has a small loop:&lt;/p>
&lt;p>call the model → look at the result → run tools → feed observations back → repeat until something terminal happens.&lt;/p>
&lt;p>&lt;strong>Loop engineering&lt;/strong> starts when you deliberately design cycles &lt;em>around&lt;/em> that behavior: verification loops, event-driven wakeups, improvement loops that learn from traces. LangChain&amp;rsquo;s newer framing treats these as a &lt;em>stack&lt;/em> of loops, not one magic &lt;code>while&lt;/code>.&lt;/p>
&lt;p>A well-engineered loop has anatomy:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Trigger&lt;/strong> — user request, schedule, failed test, new data, grader feedback&lt;/li>
&lt;li>&lt;strong>Goal&lt;/strong> — a specific state to reach, not &amp;ldquo;keep improving&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>State and memory&lt;/strong> — what the next cycle needs without replaying everything&lt;/li>
&lt;li>&lt;strong>Action policy&lt;/strong> — what the agent may change, call, delegate, or spend&lt;/li>
&lt;li>&lt;strong>Evidence&lt;/strong> — tests, schema validation, citations, diffs, metrics, human review&lt;/li>
&lt;li>&lt;strong>Feedback&lt;/strong> — compact, actionable description of why evidence failed&lt;/li>
&lt;li>&lt;strong>Stopping rule&lt;/strong> — success, budget, timeout, irrecoverable error, human escalation&lt;/li>
&lt;/ul>
&lt;p>The key line from beamnxw, which I keep repeating to teams:&lt;/p>
&lt;blockquote>
&lt;p>Do not loop on confidence. Loop on evidence.&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;The agent says it is done&amp;rdquo; is not a stopping condition. &amp;ldquo;The tests pass, the links resolve, the schema validates, and the reviewer approves&amp;rdquo; is.&lt;/p>
&lt;p>A prompt tells the model what to do during a call. A &lt;strong>loop&lt;/strong> specifies what the system does &lt;em>after&lt;/em> the call: how it observes results, chooses feedback, decides whether to continue, persists progress, and terminates. Prompt quality still matters. The loop turns a one-shot instruction into a managed process.&lt;/p>
&lt;p>Tradeoff: every grader, reviewer, or retry costs latency and money. Prefer the simplest architecture that works. Add loops where the cost of failure is higher than the cost of verification.&lt;/p>
&lt;p>On kagent, those loops live &lt;strong>inside&lt;/strong> each graph node — the research agent, the writer, the reviewer — not as a vague global &amp;ldquo;try harder&amp;rdquo; policy.&lt;/p>
&lt;hr>
&lt;h2 id="layer-3--graph-engineering-the-deep-dive">Layer 3 — Graph engineering (the deep dive)&lt;/h2>
&lt;p>This is the heart of the post. Graph engineering asks a different question than harness or loop:&lt;/p>
&lt;blockquote>
&lt;p>Not only &lt;em>what&lt;/em> the agent does — &lt;em>what component is permitted to run next?&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>Steps are &lt;strong>nodes&lt;/strong>. Allowed next steps are &lt;strong>edges&lt;/strong>. Edges can express sequence, conditional branching, parallel fan-out, joins, loops, and human interrupts. &lt;strong>State traverses the graph.&lt;/strong> The topology is what makes control flow checkable — by you, by a reviewer, by Git.&lt;/p>
&lt;p>LangGraph-style systems emphasize durable execution, shared state, and human-in-the-loop control. AutoGen&amp;rsquo;s docs put it bluntly: use a graph when you need exact control over agent order, different next steps for different outcomes, deterministic branching, or complex multi-step processes with cycles.&lt;/p>
&lt;p>That is not &amp;ldquo;draw boxes because multi-agent is trendy.&amp;rdquo; That is &lt;strong>control over agents&lt;/strong>, not another abstraction for the sake of it.&lt;/p>
&lt;h3 id="what-graph-engineers-actually-decide">What graph engineers actually decide&lt;/h3>
&lt;p>beamnxw lists the real design work. This is the checklist I use when someone says &amp;ldquo;we should make it multi-agent&amp;rdquo;:&lt;/p>
&lt;p>&lt;strong>1. Node boundaries&lt;/strong>&lt;br>
Which work belongs in a deterministic function, an LLM call, a specialist agent, or a human review step?&lt;br>
If everything is &amp;ldquo;another LLM agent,&amp;rdquo; you have not drawn boundaries — you have multiplied prompts.&lt;/p>
&lt;p>&lt;strong>2. State schema&lt;/strong>&lt;br>
What may each node read or update? How do parallel updates merge?&lt;br>
If state is &amp;ldquo;whatever is in the chat log,&amp;rdquo; joins and recovery will hurt.&lt;/p>
&lt;p>&lt;strong>3. Routing conditions&lt;/strong>&lt;br>
Which &lt;em>evidence&lt;/em> sends work forward, backward, sideways, or to escalation?&lt;br>
Routing on vibes (&amp;ldquo;looks good&amp;rdquo;) is how you reintroduce invisible control flow.&lt;/p>
&lt;p>&lt;strong>4. Concurrency&lt;/strong>&lt;br>
What can run in parallel? What must join? What shared resources need coordination?&lt;br>
Fan-out without a join plan is just concurrent chaos.&lt;/p>
&lt;p>&lt;strong>5. Cycles and exits&lt;/strong>&lt;br>
Where are retries legal? How many are allowed? What makes the cycle &lt;em>safe&lt;/em>?&lt;br>
A cycle without an exit is a cost leak with extra arrows.&lt;/p>
&lt;p>&lt;strong>6. Durability&lt;/strong>&lt;br>
Where do checkpoints occur? How does execution resume after interruption?&lt;br>
Long-running multi-agent work without durability is a demo, not a system.&lt;/p>
&lt;p>If you cannot answer those six, you do not have a graph design yet. You have a slide.&lt;/p>
&lt;h3 id="when-a-graph-is-worth-the-ceremony">When a graph is worth the ceremony&lt;/h3>
&lt;p>Graphs earn their keep when the process has:&lt;/p>
&lt;ul>
&lt;li>meaningful &lt;strong>branches&lt;/strong>&lt;/li>
&lt;li>&lt;strong>parallel&lt;/strong> work that must rejoin&lt;/li>
&lt;li>&lt;strong>approvals&lt;/strong>&lt;/li>
&lt;li>&lt;strong>recovery&lt;/strong> paths&lt;/li>
&lt;li>multiple &lt;strong>specialist&lt;/strong> agents with different tools and prompts&lt;/li>
&lt;/ul>
&lt;p>They are &lt;em>less&lt;/em> useful when the job is simply &amp;ldquo;give one agent three tools and let it work.&amp;rdquo;&lt;/p>
&lt;p>There is also a real risk the article calls out: &lt;strong>a graph can freeze assumptions too early&lt;/strong>. If the model must dynamically invent the plan, forcing every possible path into a diagram can make the system more brittle, not less. Formalize paths you have observed. Do not invent a twenty-node topology on day one because the architecture blog had pretty boxes.&lt;/p>
&lt;h3 id="how-the-three-layers-nest-in-one-system">How the three layers nest in one system&lt;/h3>
&lt;p>Notice the nesting carefully:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>graph&lt;/strong> runs with support from the harness&lt;/li>
&lt;li>One or more &lt;strong>loops&lt;/strong> live &lt;em>inside&lt;/em> the graph (usually inside nodes)&lt;/li>
&lt;li>The &lt;strong>harness&lt;/strong> supplies the state, tools, sandboxes, and evaluators those loops need&lt;/li>
&lt;/ul>
&lt;p>Categories overlap because software layers overlap. Each still gives you a different lever when the system fails:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>If you see…&lt;/th>
&lt;th>Fix this layer first&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cannot resume, loses workspace, tool surface is a mess, permissions are wrong&lt;/td>
&lt;td>&lt;strong>Harness&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Runs forever, no proof of success, retries have no budget&lt;/td>
&lt;td>&lt;strong>Loop&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Wrong specialist runs, no approval path, unreadable flow, no recovery route&lt;/td>
&lt;td>&lt;strong>Graph&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Do not blame the model for orchestration failures. A model cannot compensate reliably for stale state, ambiguous tool schemas, broken APIs, or missing exit conditions. Improve the layer that owns the failure.&lt;/p>
&lt;h3 id="expensive-mistakes-steal-these-warnings">Expensive mistakes (steal these warnings)&lt;/h3>
&lt;p>These come almost straight from the same guide, and I have watched all of them in real projects:&lt;/p>
&lt;p>&lt;strong>Building a graph before understanding the work.&lt;/strong>&lt;br>
Teams translate a business process into dozens of nodes before they have watched a capable agent solve it. Start with traces from a simpler harness. Formalize the stable paths.&lt;/p>
&lt;p>&lt;strong>Letting the same model write and grade without safeguards.&lt;/strong>&lt;br>
Self-review can help, but it shares blind spots. Prefer deterministic checks where possible, separate reviewer context, and require humans for high-impact actions.&lt;/p>
&lt;p>&lt;strong>Using &amp;ldquo;keep trying&amp;rdquo; as a loop specification.&lt;/strong>&lt;br>
Unbounded retry is a cost leak. Every loop needs a measurable objective, fresh evidence, max attempts, and a named escalation path.&lt;/p>
&lt;p>&lt;strong>Treating the harness as a dumping ground.&lt;/strong>&lt;br>
More tools and memory are not automatically better. Crowded toolsets raise selection errors. Noisy context raises confusion. Broad permissions raise risk.&lt;/p>
&lt;p>&lt;strong>Blaming the model for orchestration failures.&lt;/strong>&lt;br>
If the graph has no exits and the harness loses state, a better model will not save you.&lt;/p>
&lt;hr>
&lt;h2 id="mapping-graph-engineering-onto-kagent--substrate">Mapping graph engineering onto kagent + substrate&lt;/h2>
&lt;p>Now the practical part: how do those abstract graph decisions show up as Kubernetes resources?&lt;/p>
&lt;h3 id="the-split-of-ownership">The split of ownership&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> You / UI / Git / CronJob
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kagent — graph control plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (nodes, edges, models, MCP, HITL)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ create / resume actor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Agent Substrate — harness density
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (WorkerPool, gVisor, snapshots)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Running actors
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (loops inside each node session)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>kagent&lt;/strong> owns topology: which agents exist, what they may call, which model they use, when a human must approve&lt;/li>
&lt;li>&lt;strong>Substrate&lt;/strong> owns dense, isolated execution: suspend idle sessions, restore sub-second, sandbox with gVisor&lt;/li>
&lt;li>&lt;strong>Kubernetes&lt;/strong> owns platform primitives: CRDs, CronJobs, GitOps, RBAC&lt;/li>
&lt;/ul>
&lt;p>Put simply:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>kagent makes the graph declarative. Substrate makes idle nodes cheap and isolated. Loops still live inside the nodes.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;h3 id="graph-decisions--concrete-objects">Graph decisions → concrete objects&lt;/h3>
&lt;p>Here is beamnxw&amp;rsquo;s design list, translated into this stack:&lt;/p>
&lt;p>&lt;strong>Node boundaries&lt;/strong>&lt;br>
A specialist is a &lt;code>SandboxAgent&lt;/code> (declarative Go ADK on substrate) or an &lt;code>AgentHarness&lt;/code> (coding backends like OpenClaw/Hermes). A deterministic step might stay outside the LLM entirely. A human step is &lt;code>requireApproval&lt;/code> on a tool — or a separate operator-facing path. Do not make every box an LLM if a function or a human is the right node type.&lt;/p>
&lt;p>&lt;strong>State schema&lt;/strong>&lt;br>
Prefer explicit memory, prompt templates, and session/ACP state over &amp;ldquo;the whole chat forever.&amp;rdquo; Each node should know what it is allowed to read and what it is allowed to write. Parallel specialists need a merge story (orchestrator collects structured outputs; do not hope free-form prose merges cleanly).&lt;/p>
&lt;p>&lt;strong>Routing conditions&lt;/strong>&lt;br>
In a declarative multi-agent setup on kagent, routing often lives in the orchestrator&amp;rsquo;s system message &lt;em>plus&lt;/em> the hard constraint of which agents appear as tools. That is weaker than a pure state-machine framework for complex branching — and that is fine if your edges are few and stable. For high-stakes branching, make the condition evidence-based in the prompt &lt;em>and&lt;/em> keep the legal next agents limited to the tool list. Illegal edges simply do not exist.&lt;/p>
&lt;p>&lt;strong>Concurrency&lt;/strong>&lt;br>
If two specialists can run without shared mutable state, you can fan out (sequentially via orchestrator today, or with explicit parallel patterns as your stack grows). Size the &lt;strong>WorkerPool&lt;/strong> for peak concurrent &lt;em>running&lt;/em> actors, not for total nodes in the graph.&lt;/p>
&lt;p>&lt;strong>Cycles and exits&lt;/strong>&lt;br>
A research ↔ revise cycle is an edge back to writer/reviewer with a max iteration count in the orchestrator policy. Without a max and an escalation edge (human), you reinvent unbounded &amp;ldquo;keep trying.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Durability&lt;/strong>&lt;br>
This is where substrate shines. Idle graph nodes do not need to hold pods. Actors checkpoint RAM + filesystem to object storage and resume when the next edge fires. Graph durability is &amp;ldquo;the session can survive interruption&amp;rdquo;; Kubernetes CronJobs handle &amp;ldquo;start this path on a schedule.&amp;rdquo;&lt;/p>
&lt;h3 id="edges-you-can-review-in-git">Edges you can review in Git&lt;/h3>
&lt;p>In kagent, an edge is usually one of:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Agent-as-tool&lt;/strong> — orchestrator may call &lt;code>research-node&lt;/code>&lt;/li>
&lt;li>&lt;strong>MCP tool binding&lt;/strong> — node may call a named tool on a ToolServer&lt;/li>
&lt;li>&lt;strong>HITL gate&lt;/strong> — tool requires human approval before it runs&lt;/li>
&lt;/ol>
&lt;p>That is the difference between &amp;ldquo;the supervisor will figure it out&amp;rdquo; and &amp;ldquo;the allowed topology is in the CRD.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h2 id="building-the-graph-on-this-stack">Building the graph on this stack&lt;/h2>
&lt;h3 id="install-order-harness-under-the-graph">Install order (harness under the graph)&lt;/h3>
&lt;p>Install &lt;strong>Agent Substrate&lt;/strong> first (&lt;code>ate-system&lt;/code>), then &lt;strong>kagent&lt;/strong> with substrate enabled, then a &lt;strong>WorkerPool&lt;/strong>, then &lt;code>ModelConfig&lt;/code>, then the agents that form your graph.&lt;/p>
&lt;p>I will not re-paste every Helm flag — see the &lt;a href="https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/">kind guide&lt;/a> and &lt;a href="https://maniak.io/articles/2026-07-13-kagent-oss-agent-substrate-kind-guide/">code share&lt;/a>. Constraint worth knowing early: substrate-backed declarative agents want the &lt;strong>Go&lt;/strong> runtime today.&lt;/p>
&lt;h3 id="declare-nodes-as-sandboxagents">Declare nodes as SandboxAgents&lt;/h3>
&lt;p>A node is an honest job description: system message, model, tools, pool.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SandboxAgent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">research-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Research specialist — structured facts only&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You research a topic and return structured facts only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Prefer tools over guesswork. Cite sources when you can.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Stop when the evidence is enough — do not invent certainty.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">substrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">substrate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workerPoolRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Writer and reviewer nodes are the same shape with different prompts and tool allowlists. That &lt;em>is&lt;/em> node boundary design.&lt;/p>
&lt;h3 id="draw-edges-with-agent-as-tool">Draw edges with agent-as-tool&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SandboxAgent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">briefing-orchestrator&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Research → draft → review for an industry briefing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You own the briefing pipeline.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1) Call research-node. Require structured facts with sources.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2) Call writer-node with those facts only.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3) Call reviewer-node. If evidence fails, send feedback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> to writer-node at most twice, then escalate to a human.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Never invent a specialist that is not in your tool list.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">research-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">writer-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reviewer-node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">substrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">substrate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workerPoolRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Read that system message again as &lt;strong>graph policy&lt;/strong>: sequence, evidence-based cycle with a max, escalation exit, and a hard tool list so illegal edges do not exist. That is loop design living inside a graph node — exactly the nesting the three-layer model describes.&lt;/p>
&lt;h3 id="human-interrupts-as-real-nodes">Human interrupts as real nodes&lt;/h3>
&lt;p>High-impact tools should not be free edges. With kagent, &lt;code>requireApproval&lt;/code> pauses execution until a person decides. That is a human interrupt in graph terms — see &lt;a href="https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/">HITL on kagent&lt;/a>. Prefer it for publish, delete, deploy, and spend. Leave pure reads free so the graph does not drown in clicks.&lt;/p>
&lt;h3 id="schedules-cronjob-as-the-missing-trigger">Schedules: CronJob as the missing trigger&lt;/h3>
&lt;p>Neither kagent nor substrate is primarily a calendar product. Kubernetes already is.&lt;/p>
&lt;p>Keep nodes as CRDs (mostly idle, cheap on substrate). Use a &lt;code>CronJob&lt;/code> to fire the orchestrator on a schedule. Substrate resumes the actor, the graph runs, the actor suspends again.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">batch/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CronJob&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">daily-briefing-trigger&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0 8 * * *&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jobTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">restartPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OnFailure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">trigger&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">curlimages/curl:8.5.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">sS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">X&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">http://kagent-controller.kagent.svc:8083/api/a2a/kagent/briefing-orchestrator/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Adjust path, body, and auth for your environment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Trigger from CronJob. Topology from CRDs. Density from substrate. No fourth control plane for &amp;ldquo;08:00.&amp;rdquo;&lt;/p>
&lt;h3 id="why-sparse-graphs-stay-cheap">Why sparse graphs stay cheap&lt;/h3>
&lt;p>Most multi-agent graphs are sparse in &lt;em>time&lt;/em>. Research runs, then sleeps. Writer runs, then sleeps. Reviewer runs for a moment. Orchestrator waits on a human.&lt;/p>
&lt;p>Pod-per-agent pays for the worst case continuously. Substrate pays for concurrent &lt;strong>running&lt;/strong> work:&lt;/p>
&lt;ol>
&lt;li>Edge fires / request arrives&lt;/li>
&lt;li>Actor restores onto a free WorkerPool pod&lt;/li>
&lt;li>Node loop runs inside gVisor&lt;/li>
&lt;li>Idle → checkpoint to object storage → worker returns to the pool&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale workerpool kagent-default -n kagent --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">5&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Scale concurrency, not &amp;ldquo;number of boxes in the architecture diagram.&amp;rdquo;&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-research-and-publish-briefing">Worked example: research-and-publish briefing&lt;/h2>
&lt;p>beamnxw uses a research-and-publishing agent as the running example of how layers nest. Here is the same story on kagent + substrate.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">CronJob&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="n">user&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="n">briefing&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">orchestrator&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">├─▶&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">research&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="err">──&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">structured&lt;/span> &lt;span class="n">facts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">├─▶&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">writer&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="err">──&lt;/span> &lt;span class="n">draft&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="n">facts&lt;/span> &lt;span class="n">only&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└─▶&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">reviewer&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">node&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="err">──&lt;/span> &lt;span class="n">evidence&lt;/span> &lt;span class="n">checks&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">pass&lt;/span>&lt;span class="err">?&lt;/span> &lt;span class="err">─┼─&lt;/span> &lt;span class="n">yes&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">publish&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">optional&lt;/span> &lt;span class="n">HITL&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└─&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">feedback&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">writer&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nb">max&lt;/span> &lt;span class="n">N&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">then&lt;/span> &lt;span class="n">escalate&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">human&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Graph decisions in this design:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nodes:&lt;/strong> orchestrator, research, writer, reviewer, human (on publish)&lt;/li>
&lt;li>&lt;strong>Edges:&lt;/strong> agent-as-tool refs; optional cycle writer ↔ reviewer; escalate edge&lt;/li>
&lt;li>&lt;strong>State:&lt;/strong> structured facts object, draft, review findings — not one blob of chat&lt;/li>
&lt;li>&lt;strong>Routing:&lt;/strong> reviewer evidence decides pass / revise / escalate&lt;/li>
&lt;li>&lt;strong>Cycles:&lt;/strong> max two revise loops, then human&lt;/li>
&lt;li>&lt;strong>Durability:&lt;/strong> each specialist session is a substrate actor; idle does not hold a pod&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Loop decisions:&lt;/strong> mostly inside reviewer — grade against sources, return compact feedback, stop on pass, budget, or escalate.&lt;/p>
&lt;p>&lt;strong>Harness decisions:&lt;/strong> gVisor isolation, tight MCP tool lists per node, OTel traces, snapshots so a long briefing can pause on HITL without burning a warm Deployment overnight.&lt;/p>
&lt;p>If you only remember one sequencing rule from the article: &lt;strong>do not draw this whole graph on day one.&lt;/strong> Start with a single agent and traces. Promote stable handoffs into CRDs when you see them repeating.&lt;/p>
&lt;hr>
&lt;h2 id="questions-worth-asking-before-you-scale-this">Questions worth asking before you scale this&lt;/h2>
&lt;p>Before you grow the graph, sit with a few questions. They are adapted from the production checklist in the beamnxw guide — translated into this stack.&lt;/p>
&lt;p>&lt;strong>On the graph:&lt;/strong> Which paths must be deterministic? Where can work run in parallel? What state is shared, and who merges it? Where are the human gates and recovery routes? Could a teammate see the legal topology from the YAML alone?&lt;/p>
&lt;p>&lt;strong>On the loops:&lt;/strong> What evidence proves success for each node? What feedback comes back on failure? How many retries? What happens when the budget is gone?&lt;/p>
&lt;p>&lt;strong>On the harness:&lt;/strong> Are tools few, clear, and observable? Does state survive suspend/resume? Are permissions tight? Can someone pause, inspect, and resume a run without redeploying the world?&lt;/p>
&lt;p>&lt;strong>On ops:&lt;/strong> Can you replay a bad run and point at a CRD change that fixed it? Are you watching cost, latency, failure rate, and how often a human has to step in?&lt;/p>
&lt;p>If you cannot answer the graph questions, a bigger model will not save you. If you cannot answer the harness questions, more nodes will not either.&lt;/p>
&lt;hr>
&lt;h2 id="when-you-should-not-build-a-graph">When you should &lt;em>not&lt;/em> build a graph&lt;/h2>
&lt;p>If the job is &amp;ldquo;one agent, three tools, let it work,&amp;rdquo; you do not need twelve nodes. Build a solid harness and a tight loop first.&lt;/p>
&lt;p>Add a multi-node graph when you keep reimplementing the same branches, specialist handoffs, recovery paths, or human gates in glue code — and when those paths are stable enough that freezing them is a feature, not a trap.&lt;/p>
&lt;p>Graph engineering is for control you can inspect. It is not a fashion statement.&lt;/p>
&lt;hr>
&lt;h2 id="bottom-line">Bottom line&lt;/h2>
&lt;p>Steal the three-layer framing and use it on purpose:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Harness&lt;/strong> — the environment that makes the model operable&lt;/li>
&lt;li>&lt;strong>Loop&lt;/strong> — the iterative process with evidence and stop rules&lt;/li>
&lt;li>&lt;strong>Graph&lt;/strong> — the explicit map of what may run next&lt;/li>
&lt;/ul>
&lt;p>None of them replaces the others. A pretty graph with a broken harness still loses state. A great harness with no stop rules still burns money. Careful loops still turn to spaghetti when branching and approvals live only in ad-hoc code.&lt;/p>
&lt;p>On Kubernetes, the split is clean enough to ship:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>kagent&lt;/strong> makes the graph (and a lot of the policy) declarative&lt;/li>
&lt;li>&lt;strong>Agent Substrate&lt;/strong> makes idle nodes cheap and isolated&lt;/li>
&lt;li>&lt;strong>You&lt;/strong> still design node boundaries, evidence, and exits&lt;/li>
&lt;li>&lt;strong>CronJobs&lt;/strong> handle &amp;ldquo;run this path at 8am&amp;rdquo; without inventing a new product&lt;/li>
&lt;/ul>
&lt;p>Keep the topology in Git. Scale the WorkerPool for concurrent work, not for diagram vanity. Put humans on tools that can hurt.&lt;/p>
&lt;hr>
&lt;h2 id="where-to-start-call-to-action">Where to start (call to action)&lt;/h2>
&lt;p>You do not need the full briefing pipeline on day one. Do this instead:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Stand up the harness.&lt;/strong> Install Agent Substrate, then kagent with substrate enabled, then a small WorkerPool. Follow the hands-on path:&lt;br>
&lt;a href="https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/">Suspend &amp;amp; resume on kind&lt;/a> · &lt;a href="https://maniak.io/articles/2026-07-13-kagent-oss-agent-substrate-kind-guide/">OSS substrate code share&lt;/a>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Ship one node you trust.&lt;/strong> One &lt;code>SandboxAgent&lt;/code>, one model, a short tool list. Chat with it. Watch it suspend and resume. Get comfortable before you draw edges.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Add edges only when traces demand them.&lt;/strong> Promote repeated handoffs into agent-as-tool refs. Keep illegal specialists off the tool list so they cannot run by accident.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Put a human on the dangerous tools.&lt;/strong> Start with &lt;a href="https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/">HITL on kagent&lt;/a>. Approve deletes and deploys. Leave reads free.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Put the YAML in Git.&lt;/strong> Treat prompt, model, and tool changes like production config. &lt;a href="https://maniak.io/articles/2026-04-02-gitops-for-agents-deployment-and-management/">GitOps for agents&lt;/a> is the longer version of that argument.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Schedule later.&lt;/strong> When you need calendar wakeups, add a CronJob that triggers the orchestrator. Do not wait for a native &amp;ldquo;agent cron&amp;rdquo; product.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Runnable demos live in &lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">&lt;code>sebbycorp/kagent-demos&lt;/code>&lt;/a>. Upstream docs: &lt;a href="https://kagent.dev">kagent.dev&lt;/a> · &lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a> · &lt;a href="https://github.com/kagent-dev/kagent">github.com/kagent-dev/kagent&lt;/a>. The conceptual spine for this post is still &lt;a href="https://x.com/beamnxw/status/2081022966645535079">beamnxw&amp;rsquo;s three-layer guide&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Your move:&lt;/strong> clone the demo, run one substrate-backed agent on kind this week, and only then decide which edges belong in the graph. Topology you have not earned from traces is just decoration.&lt;/p></content:encoded></item><item><title>agentgateway 1.4.0 OSS: what changed since 1.3.0</title><link>https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/</link><pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/</guid><description>&lt;p>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway&lt;/a> &lt;strong>v1.4.0&lt;/strong> landed on July 27, 2026.&lt;/p>
&lt;p>If you are still on 1.3.x, this is the upgrade map. It tracks the &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.4.0">official v1.4.0 release notes&lt;/a> — highlights, breaking changes, security, and the day-2 bits that actually matter — plus a bit of operator commentary.&lt;/p>
&lt;p>&lt;strong>Also useful&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.3.0">v1.3.0&lt;/a> · &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.3.1">v1.3.1&lt;/a>&lt;/li>
&lt;li>Diff: &lt;a href="https://github.com/agentgateway/agentgateway/compare/v1.3.1...v1.4.0">v1.3.1&amp;hellip;v1.4.0&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Install bits&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Images: &lt;code>cr.agentgateway.dev/agentgateway:v1.4.0&lt;/code>, &lt;code>cr.agentgateway.dev/controller:v1.4.0&lt;/code> (glibc — &lt;strong>no more musl image tags&lt;/strong>)&lt;/li>
&lt;li>Charts: &lt;code>agentgateway&lt;/code>, &lt;code>agentgateway-crds&lt;/code>, &lt;code>agentgateway-standalone&lt;/code>&lt;/li>
&lt;li>Binaries: still built with musl; proxy + &lt;code>agctl&lt;/code>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="the-short-version">The short version&lt;/h2>
&lt;p>Upstream’s own highlight reel:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Full MCP &lt;code>2026-07-28&lt;/code> support&lt;/strong> (and a year of community work behind it)&lt;/li>
&lt;li>&lt;strong>Cross App Access / Enterprise-Managed Authorization for MCP&lt;/strong> (ID-JAG)&lt;/li>
&lt;li>&lt;strong>OAuth token exchange&lt;/strong> backend auth (RFC 8693 + JWT bearer)&lt;/li>
&lt;li>&lt;strong>Standalone &lt;code>gateways&lt;/code>&lt;/strong> + DB-backed UI config (sqlite / postgres) + &lt;strong>standalone Helm chart&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Experimental &lt;code>AgentgatewayModel&lt;/code>&lt;/strong> on Kubernetes (off by default)&lt;/li>
&lt;li>Gateway API &lt;strong>v1.6&lt;/strong>, fault injection, richer guardrails, lots of fixes&lt;/li>
&lt;/ol>
&lt;p>My add-on for operators: &lt;strong>read the breaking + security sections before you helm upgrade.&lt;/strong> There is a High severity MCP auth advisory in this release.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Area&lt;/th>
&lt;th>What moved&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>MCP&lt;/td>
&lt;td>Protocol 2026-07-28, Apps, traces via &lt;code>_meta&lt;/code>, Entra/Descope/authentik&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Auth&lt;/td>
&lt;td>Token exchange, Cross App Access (incl. MCP EMA), hashed API keys&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Standalone&lt;/td>
&lt;td>&lt;code>gateways&lt;/code> supersede &lt;code>binds&lt;/code>, UI on same port, sqlite/postgres storage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>UI&lt;/td>
&lt;td>Settings to bind console + attach OIDC/JWT/API key/CSRF/CORS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kubernetes&lt;/td>
&lt;td>Experimental &lt;code>AgentgatewayModel&lt;/code> (off by default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Platform&lt;/td>
&lt;td>DaemonSet, Gateway API 1.6 / TCPRoute v1, monitors, delay fault injection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Security&lt;/td>
&lt;td>&lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">GHSA-mvgg-jvj2-4frq&lt;/a> + CEL body guidance&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="read-this-first-breaking-changes">Read this first: breaking changes&lt;/h2>
&lt;p>Straight from upstream. Do these before you call the upgrade done.&lt;/p>
&lt;h3 id="1-gateway-api-v16-and-tcproute-v1">1. Gateway API v1.6 and TCPRoute v1&lt;/h3>
&lt;p>agentgateway builds against &lt;strong>Gateway API v1.6&lt;/strong>. The controller uses &lt;strong>&lt;code>TCPRoute&lt;/code> v1&lt;/strong>, not &lt;code>v1alpha2&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> re-apply the Gateway API CRDs that match this release &lt;strong>before&lt;/strong> you upgrade the controller/proxy.&lt;/p>
&lt;h3 id="2-mcp-request-phase-guardrail-rejections-return-http-200">2. MCP request-phase guardrail rejections return HTTP 200&lt;/h3>
&lt;p>If an MCP guardrail rejects in the &lt;strong>request&lt;/strong> phase, the gateway now returns &lt;strong>HTTP 200&lt;/strong> with a JSON-RPC error body — same as the response phase.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> fix clients/tests that expected a non-200 on request-phase rejects.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/mcp/guardrails/">k8s MCP guardrails&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/mcp/guardrails/">standalone&lt;/a>&lt;/p>
&lt;h3 id="3-standalone-authlocation-no-longer-nests-expression">3. Standalone &lt;code>auth.location&lt;/code> no longer nests &lt;code>expression&lt;/code>&lt;/h3>
&lt;p>Custom token location config dropped the double-nested &lt;code>expression&lt;/code> field. Use the flattened form.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> grep your standalone policies for nested &lt;code>auth.location&lt;/code> expressions and update them.&lt;/p>
&lt;h3 id="4-musl-container-images-removed">4. musl container images removed&lt;/h3>
&lt;p>Musl &lt;strong>image&lt;/strong> variants are gone. Use standard glibc images. &lt;strong>Binary&lt;/strong> releases are still musl builds.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> update image pins, digests, and SBOMs.&lt;/p>
&lt;hr>
&lt;h2 id="read-this-second-security">Read this second: security&lt;/h2>
&lt;h3 id="high-mcp-sessions-crossing-routes-ghsa-mvgg-jvj2-4frq">High: MCP sessions crossing routes (GHSA-mvgg-jvj2-4frq)&lt;/h3>
&lt;p>This release fixes &lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">GHSA-mvgg-jvj2-4frq&lt;/a> — &lt;strong>High (8.1)&lt;/strong> — where stateful MCP sessions could cross routes and overwrite authorization policy.&lt;/p>
&lt;p>Read the advisory for impact and mitigation. Credit: &lt;a href="https://github.com/0dd">Aonan Guan&lt;/a>.&lt;/p>
&lt;p>Related hardening in the same train includes pinning MCP sessions to a backend and tightening passthrough checks. Still: if you run MCP with authz on 1.3, treat 1.4 as a &lt;strong>security upgrade&lt;/strong>, not just a feature bump.&lt;/p>
&lt;h3 id="cel-requestbody--responsebody-in-auth-policies">CEL &lt;code>request.body&lt;/code> / &lt;code>response.body&lt;/code> in auth policies&lt;/h3>
&lt;p>Upstream flags a footgun (recommendation + behavior change), also from the same reporter.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>1.3.x and earlier&lt;/th>
&lt;th>1.4&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>request.body&lt;/code>&lt;/td>
&lt;td>Truncated to &lt;code>http.maxBufferSize&lt;/code> (default 2MB)&lt;/td>
&lt;td>&lt;strong>Not available if truncated&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>request.truncatedBody&lt;/code>&lt;/td>
&lt;td>Not available&lt;/td>
&lt;td>Truncated to max buffer size&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Why it matters: a policy like &lt;code>string(request.truncatedBody).contains(&amp;quot;attacker-payload&amp;quot;)&lt;/code> can miss a match when the body is over 2MB, or when encoding/compression changes what you thought you were matching. Auth on raw bodies is easy to get wrong.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> audit CEL auth that touches bodies. Prefer not using body contents for hard authorization when you can avoid it. If you must, handle the “body missing / truncated” case explicitly.&lt;/p>
&lt;hr>
&lt;h2 id="what-13-already-gave-you">What 1.3 already gave you&lt;/h2>
&lt;p>v1.3.0 brought the rebuilt UI (LLM / MCP / Traffic), cost tracking, virtual models, reusable providers/guardrails, more LLM providers, body buffering, better CEL/&lt;code>agctl&lt;/code>, better traces.&lt;/p>
&lt;p>v1.3.1 was polish: controller &lt;code>podLabels&lt;/code>, Vertex multi-region, same port with different protocols across gateways, UI wildcard/virtual-model fixes.&lt;/p>
&lt;p>1.3.1 does not force a redesign. &lt;strong>1.4 does&lt;/strong> — config shape, CRDs, MCP auth behavior, and at least one security-fixing upgrade path.&lt;/p>
&lt;hr>
&lt;h2 id="1-mcp-protocol-2026-07-28">1. MCP protocol 2026-07-28&lt;/h2>
&lt;p>This is highlight #1 in the official notes.&lt;/p>
&lt;p>agentgateway adds support for the upcoming &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/">MCP &lt;code>2026-07-28&lt;/code> protocol&lt;/a>:&lt;/p>
&lt;ul>
&lt;li>Stateful &lt;strong>and&lt;/strong> stateless servers (SEP-2575 server-stateless gap closed; skip synthetic &lt;code>initialize&lt;/code> for modern requests)&lt;/li>
&lt;li>Trace context through MCP &lt;strong>&lt;code>_meta&lt;/code>&lt;/strong> (SEP-414 / &lt;code>traceparent&lt;/code>) — stdio backends stay on the trace&lt;/li>
&lt;li>Basic &lt;strong>MCP Apps&lt;/strong> (plus multiplexing fixes for app-originated tool calls)&lt;/li>
&lt;li>Multi-target subscriptions/listen, opaque URI multiplexing, preserved multi-resource tool-result capabilities&lt;/li>
&lt;/ul>
&lt;p>Most of this is new enough that dedicated guides are still thin. Use the &lt;a href="https://agentgateway.dev/docs/kubernetes/main/reference/api/">Kubernetes API reference&lt;/a> and &lt;a href="https://agentgateway.dev/docs/standalone/main/reference/configuration/">Standalone config reference&lt;/a> for fields available today.&lt;/p>
&lt;h3 id="new-mcp-identity-providers">New MCP identity providers&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Microsoft Entra ID&lt;/strong> (k8s + standalone) — bridges Entra vs MCP-auth mismatches (OIDC discovery metadata, strip RFC 8707 &lt;code>resource&lt;/code>, short-circuit DCR with your pre-registered app id)&lt;/li>
&lt;li>&lt;strong>Descope&lt;/strong> and &lt;strong>authentik&lt;/strong> on standalone&lt;/li>
&lt;li>Okta was already first-class in 1.3&lt;/li>
&lt;/ul>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/mcp/auth/">k8s MCP auth&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/mcp/mcp-authn/">standalone MCP auth&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/integrations/auth/descope/">Descope guide&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="2-cross-app-access-for-mcp-and-backends">2. Cross App Access (for MCP and backends)&lt;/h2>
&lt;p>Official name in the MCP world: &lt;a href="https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization">Enterprise-Managed Authorization&lt;/a>. Implementation path: OAuth Identity Assertion Authorization Grant — &lt;strong>Cross App Access / ID-JAG&lt;/strong>.&lt;/p>
&lt;p>An enterprise IdP can broker access between a client app and an MCP server (or other backend) without a separate interactive OAuth dance for every downstream app.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/security/backend-authn-cross-app-access/">k8s Cross App Access&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/configuration/security/backend-authn/cross-app-access/">standalone&lt;/a>&lt;/p>
&lt;p>Examples landed for Keycloak, xaa.dev, token exchange, and JWT bearer. Subject token source is configurable.&lt;/p>
&lt;hr>
&lt;h2 id="3-oauth-token-exchange-backend-auth">3. OAuth token exchange backend auth&lt;/h2>
&lt;p>The gateway can exchange an incoming token for a backend credential using &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693">RFC 8693&lt;/a> token exchange and &lt;a href="https://datatracker.ietf.org/doc/html/rfc7523">RFC 7523&lt;/a> JWT bearer.&lt;/p>
&lt;p>Also in this release:&lt;/p>
&lt;ul>
&lt;li>Kubernetes controller support&lt;/li>
&lt;li>Custom token types + OAuth 2.1-ish exchange defaults&lt;/li>
&lt;li>Inject &lt;strong>multiple&lt;/strong> secret-sourced headers&lt;/li>
&lt;li>Override the resolved secret key for OAuth/GCP-style refs&lt;/li>
&lt;/ul>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/security/backend-authn-oauth/">k8s OAuth token exchange&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/configuration/security/backend-authn/oauth-token-exchange/">standalone&lt;/a>&lt;/p>
&lt;p>If your 1.3 setup is “edge JWT ok, upstream still sees a shared service account,” this is the fix.&lt;/p>
&lt;h3 id="related-auth-extras">Related auth extras&lt;/h3>
&lt;ul>
&lt;li>Virtual keys from &lt;strong>ConfigMaps&lt;/strong> (not only Secrets)&lt;/li>
&lt;li>API keys stored as &lt;strong>SHA-256 hashes&lt;/strong> (raw key need not live in plaintext config)&lt;/li>
&lt;li>Admin IP allowlist + timing-attack fixes&lt;/li>
&lt;li>&lt;code>private_key_jwt&lt;/code> with certificate credentials&lt;/li>
&lt;li>AWS assume-role: session tags + &lt;code>RoleSessionName&lt;/code> / &lt;code>sessionNameExpression&lt;/code> via CEL (e.g. propagate &lt;code>jwt.sub&lt;/code>)&lt;/li>
&lt;li>Per-backend SigV4 region&lt;/li>
&lt;li>Frontend TLS with &lt;strong>multiple client CAs&lt;/strong>&lt;/li>
&lt;li>Bundled certs&lt;/li>
&lt;li>Cloud auth fetch timeouts&lt;/li>
&lt;/ul>
&lt;p>Virtual keys docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/llm/cost-controls/virtual-keys/">k8s&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/llm/cost-controls/virtual-keys/">standalone&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="4-standalone-gateways-storage-helm">4. Standalone: &lt;code>gateways&lt;/code>, storage, Helm&lt;/h2>
&lt;h3 id="gateways-replace-binds">&lt;code>gateways&lt;/code> replace &lt;code>binds&lt;/code>&lt;/h3>
&lt;p>New top-level &lt;strong>&lt;code>gateways&lt;/code>&lt;/strong> unifies UI, LLM, MCP, and ordinary routes on one listener/port. That is what makes &lt;strong>OIDC on the UI&lt;/strong> a first-class story.&lt;/p>
&lt;p>Important nuance from upstream:&lt;/p>
&lt;ul>
&lt;li>&lt;code>gateways&lt;/code> &lt;strong>supersedes&lt;/strong> &lt;code>binds&lt;/code>&lt;/li>
&lt;li>Existing &lt;strong>&lt;code>binds&lt;/code> still work&lt;/strong>&lt;/li>
&lt;li>The UI offers a &lt;strong>one-click migration&lt;/strong> from binds → gateways&lt;/li>
&lt;/ul>
&lt;p>Also: simpler host/TLS config, LLM+MCP on the same port, internal bind mode with wildcard fallback.&lt;/p>
&lt;p>Config reference: &lt;a href="https://agentgateway.dev/docs/standalone/main/reference/configuration/">standalone configuration&lt;/a>&lt;/p>
&lt;h3 id="database-backed-config-no-pvc-required">Database-backed config (no PVC required)&lt;/h3>
&lt;p>UI-created config can live in a database:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>sqlite&lt;/strong> for local&lt;/li>
&lt;li>&lt;strong>postgres&lt;/strong> for remote / HA&lt;/li>
&lt;/ul>
&lt;p>That avoids needing a persistent disk for writable UI workflows. Hybrid mode, policy writes, multi-replica notify, and “log errors to SQL too” landed in the same train. Storage naming moved away from the old configStore wording.&lt;/p>
&lt;p>Admin interface is &lt;strong>not&lt;/strong> exposed by default.&lt;/p>
&lt;h3 id="standalone-helm-chart">Standalone Helm chart&lt;/h3>
&lt;p>&lt;code>cr.agentgateway.dev/charts/agentgateway-standalone:v1.4.0&lt;/code> is real: modes for file vs database, gateways-oriented defaults, OIDC secret support, metrics service + ServiceMonitor.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/standalone/main/deployment/helm/">Standalone Helm&lt;/a>&lt;/p>
&lt;p>Treat 1.4 as the first serious baseline for that chart, not a soft bump from early experiments.&lt;/p>
&lt;hr>
&lt;h2 id="5-ui-settings-protect-the-console">5. UI Settings: protect the console&lt;/h2>
&lt;p>In the product UI: &lt;strong>Tools → Settings → UI Settings&lt;/strong>.&lt;/p>
&lt;p>Bind the console to a traffic gateway, then attach the same policy toolkit you use elsewhere.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/ui-settings.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/ui-settings.png" alt="agentgateway 1.4 UI Settings — Public UI gateway set to ui, with policy cards for OIDC, JWT, Authorization, External authz, Basic auth, API keys, CSRF, and CORS" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>You get:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Public UI gateway&lt;/strong> picker + view diff / save&lt;/li>
&lt;li>Policy cards: OIDC, JWT, Authorization, External authz, Basic auth, API keys, CSRF, CORS&lt;/li>
&lt;li>Expandable top-level policy YAML for the GitOps-minded&lt;/li>
&lt;/ul>
&lt;p>After upgrade: bind the UI to the gateway you actually expose, turn on OIDC or JWT, then put that listener on a real network.&lt;/p>
&lt;hr>
&lt;h2 id="6-experimental-agentgatewaymodel-off-by-default">6. Experimental &lt;code>AgentgatewayModel&lt;/code> (off by default)&lt;/h2>
&lt;p>Kubernetes headline — with the official caveats.&lt;/p>
&lt;p>&lt;strong>&lt;code>AgentgatewayModel&lt;/code>&lt;/strong> brings the standalone model-centric LLM experience to Kubernetes: serve multiple models on one gateway, route by model name in the request. Higher level than assembling &lt;code>AgentgatewayBackend&lt;/code> + HTTPRoute body matchers by hand.&lt;/p>
&lt;ul>
&lt;li>Short names: &lt;code>agmodel&lt;/code> / &lt;code>agentgatewaymodels.agentgateway.dev&lt;/code>&lt;/li>
&lt;li>&lt;strong>Experimental&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Off by default&lt;/strong>&lt;/li>
&lt;li>Existing APIs remain available&lt;/li>
&lt;/ul>
&lt;p>Why try it: one GitOps object per model/family, less HTTPRoute glue, k8s and standalone stop telling different stories.&lt;/p>
&lt;p>Why not force it day one: experimental. Pilot one non-prod model after the cluster is stable on 1.4.&lt;/p>
&lt;hr>
&lt;h2 id="7-guardrails-ext_proc-cel-fault-injection">7. Guardrails, ext_proc, CEL, fault injection&lt;/h2>
&lt;h3 id="guardrails">Guardrails&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;code>BackendConnectionPolicy&lt;/code>&lt;/strong> for guardrail callouts (TCP/TLS/HTTP/tunnel) on OpenAI moderation, Bedrock guardrails, Google Model Armor&lt;/li>
&lt;li>Default callout timeouts&lt;/li>
&lt;li>Clearer guardrail decisions in logs/UI&lt;/li>
&lt;li>ext_proc &lt;strong>&lt;code>failureMode&lt;/code>&lt;/strong> (fail-open / fail-closed)&lt;/li>
&lt;/ul>
&lt;h3 id="external-processing">External processing&lt;/h3>
&lt;p>Controller support for &lt;code>metadataContext&lt;/code>, &lt;code>requestAttributes&lt;/code>, &lt;code>responseAttributes&lt;/code>, plus &lt;code>failureMode&lt;/code>.&lt;/p>
&lt;h3 id="cel">CEL&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Custom CEL functions&lt;/strong> you can register for policies&lt;/li>
&lt;li>Opt-in &lt;strong>CEL filters for OTel spans&lt;/strong>&lt;/li>
&lt;li>CEL filters that decouple OTLP log fields from stdout&lt;/li>
&lt;li>&lt;code>source.connectHeaders&lt;/code> for inbound CONNECT headers&lt;/li>
&lt;li>Header transformation &lt;strong>replace&lt;/strong> mode via CEL&lt;/li>
&lt;li>Body handling split: &lt;code>body&lt;/code> vs &lt;code>truncatedBody&lt;/code> (see security section)&lt;/li>
&lt;/ul>
&lt;p>CEL refs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/reference/cel/">k8s&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/reference/cel/">standalone&lt;/a>&lt;/p>
&lt;h3 id="fault-injection-delay">Fault injection: &lt;code>delay&lt;/code>&lt;/h3>
&lt;p>New &lt;strong>delay&lt;/strong> traffic policy injects latency before forward. Duration string, CEL duration, or number-as-milliseconds. Injected delay counts against the request timeout.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/resiliency/fault-injection/">k8s fault injection&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/configuration/resiliency/fault-injection/">standalone&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="8-llm-a2a-and-provider-path-fixes">8. LLM, A2A, and provider path fixes&lt;/h2>
&lt;p>From the official “LLM gateway enhancements” plus the long PR train:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Frontend TLS&lt;/strong> multi-CA client cert validation&lt;/li>
&lt;li>&lt;strong>Bedrock:&lt;/strong> Responses→Bedrock images, 64-char tool name sanitizing, cache-write tokens in access logs&lt;/li>
&lt;li>&lt;strong>Gemini / Vertex:&lt;/strong> embeddings fix, native generateContent paths, detect-mode model/usage extraction&lt;/li>
&lt;li>&lt;strong>Azure AI Foundry:&lt;/strong> Anthropic endpoints; Responses/date-based API path fixes&lt;/li>
&lt;li>&lt;strong>A2A v1.0&lt;/strong> agent card format in URL rewriting&lt;/li>
&lt;li>Reasoning fixes on messages→completions; body decode before JSON parse&lt;/li>
&lt;li>Cost: Prometheus counter; catalog ConfigMap loading; virtual keys from ConfigMaps&lt;/li>
&lt;li>Telemetry: tool calls, A2A response metrics, richer dtrace&lt;/li>
&lt;li>Semantic routing examples (including Responses)&lt;/li>
&lt;/ul>
&lt;p>Providers: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/llm/providers/">k8s&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/llm/providers/">standalone&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="9-deployment-and-ops">9. Deployment and ops&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>DaemonSet&lt;/strong> data plane (still defaults to Deployment)&lt;/li>
&lt;li>Helm &lt;strong>&lt;code>extraContainers&lt;/code>&lt;/strong> on control plane pods&lt;/li>
&lt;li>Proxy scrape via &lt;strong>PodMonitor&lt;/strong>; standalone metrics Service + &lt;strong>ServiceMonitor&lt;/strong>&lt;/li>
&lt;li>&lt;code>agentgateway_controller_build_info&lt;/code> metric&lt;/li>
&lt;li>Standalone SQL logging for success &lt;strong>and&lt;/strong> error, with faster writes&lt;/li>
&lt;li>Internal bind mode (annotation + wildcard fallback)&lt;/li>
&lt;li>Frontend policies targetable to a Gateway port&lt;/li>
&lt;li>Buffer: stream when body exceeds limit instead of always hard-failing&lt;/li>
&lt;li>ext_authz HTTP cache + custom HTTP bodies&lt;/li>
&lt;li>&lt;code>agctl&lt;/code> much smaller; some trace/config commands under proxy subcommand; nightly artifacts&lt;/li>
&lt;li>&lt;strong>s390x&lt;/strong> support&lt;/li>
&lt;li>Embedded UI build by default (&lt;code>UI=0&lt;/code> to opt out)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="how-i-would-upgrade">How I would upgrade&lt;/h2>
&lt;h3 id="kubernetes">Kubernetes&lt;/h3>
&lt;ol>
&lt;li>Re-apply &lt;strong>Gateway API CRDs&lt;/strong> for the v1.6 / TCPRoute v1 set this release expects&lt;/li>
&lt;li>Bump &lt;strong>&lt;code>agentgateway-crds&lt;/code>&lt;/strong> → v1.4.0&lt;/li>
&lt;li>Bump controller + proxy &lt;strong>together&lt;/strong>&lt;/li>
&lt;li>If you run authenticated MCP: read &lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">GHSA-mvgg-jvj2-4frq&lt;/a> and validate session/route authz behavior&lt;/li>
&lt;li>Smoke HTTPRoutes, MCP, JWT/ext_authz, traces, guardrails&lt;/li>
&lt;li>Audit CEL auth that uses &lt;code>request.body&lt;/code> / &lt;code>response.body&lt;/code>&lt;/li>
&lt;li>Only then pilot &lt;strong>one&lt;/strong> non-prod &lt;code>AgentgatewayModel&lt;/code>&lt;/li>
&lt;li>Keep Deployment unless you truly need DaemonSet&lt;/li>
&lt;/ol>
&lt;h3 id="standalone">Standalone&lt;/h3>
&lt;ol>
&lt;li>Snapshot config&lt;/li>
&lt;li>Note: &lt;code>binds&lt;/code> still work; migrate with UI one-click or convert to &lt;code>gateways&lt;/code> deliberately&lt;/li>
&lt;li>Flatten any nested &lt;code>auth.location&lt;/code> expressions&lt;/li>
&lt;li>Pick storage: &lt;strong>sqlite&lt;/strong> local vs &lt;strong>postgres&lt;/strong> HA (drop PVC assumptions from early charts)&lt;/li>
&lt;li>Open &lt;strong>UI Settings&lt;/strong>, bind console, enable OIDC/JWT&lt;/li>
&lt;li>Install/upgrade &lt;code>agentgateway-standalone&lt;/code> v1.4.0 after reading values&lt;/li>
&lt;/ol>
&lt;h3 id="auth-heavy-estates">Auth-heavy estates&lt;/h3>
&lt;ol>
&lt;li>Inventory upstreams still on shared service accounts&lt;/li>
&lt;li>Prototype token exchange on one API&lt;/li>
&lt;li>Prototype Cross App Access for one MCP or SaaS API&lt;/li>
&lt;li>Prefer native Entra MCP provider over home-grown resource-parameter proxies&lt;/li>
&lt;li>Prefer hashed virtual keys / ConfigMap sources where you can&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="what-i-would-turn-on-first">What I would turn on first&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Security fix path&lt;/strong> — upgrade MCP authz estates for the High advisory&lt;/li>
&lt;li>&lt;strong>Gateway API CRD re-apply + clean 1.4 roll&lt;/strong>&lt;/li>
&lt;li>&lt;strong>One gateway + UI Settings&lt;/strong> — console actually protected&lt;/li>
&lt;li>&lt;strong>Token exchange or XAA&lt;/strong> — user-scoped upstream tokens&lt;/li>
&lt;li>&lt;strong>MCP 2026-07-28 + &lt;code>_meta&lt;/code> traces&lt;/strong> — federation you can debug&lt;/li>
&lt;li>&lt;strong>Experimental AgentgatewayModel&lt;/strong> — one non-prod pilot, not a big-bang rewrite&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.4.0">Official v1.4.0 release&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">Security advisory GHSA-mvgg-jvj2-4frq&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/compare/v1.3.1...v1.4.0">v1.3.1&amp;hellip;v1.4.0&lt;/a>&lt;/li>
&lt;li>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest/quickstart/">Kubernetes quick start&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/latest/quickstart/">Standalone quick start&lt;/a>&lt;/li>
&lt;li>On this site: &lt;a href="https://maniak.io/articles/2026-07-12-eight-principles-of-an-ai-gateway-agentgateway/">Eight principles of an AI gateway&lt;/a> · &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-v1-why-it-matters/">Why the v1 line matters&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>Bottom line:&lt;/strong> 1.4 is not only features. It is &lt;strong>MCP protocol catch-up&lt;/strong>, &lt;strong>real delegated auth&lt;/strong> (token exchange + Cross App Access for MCP), &lt;strong>standalone that can run UI/LLM/MCP on one port with DB-backed config&lt;/strong>, an &lt;strong>experimental model API&lt;/strong> on Kubernetes, and a set of &lt;strong>breaking + security fixes you should not skip&lt;/strong>.&lt;/p>
&lt;p>Re-apply Gateway API CRDs. Bump charts. Read the MCP session advisory. Bind and lock down the UI. Then play with AgentgatewayModel — in that order.&lt;/p></description><content:encoded>&lt;p>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway&lt;/a> &lt;strong>v1.4.0&lt;/strong> landed on July 27, 2026.&lt;/p>
&lt;p>If you are still on 1.3.x, this is the upgrade map. It tracks the &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.4.0">official v1.4.0 release notes&lt;/a> — highlights, breaking changes, security, and the day-2 bits that actually matter — plus a bit of operator commentary.&lt;/p>
&lt;p>&lt;strong>Also useful&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.3.0">v1.3.0&lt;/a> · &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.3.1">v1.3.1&lt;/a>&lt;/li>
&lt;li>Diff: &lt;a href="https://github.com/agentgateway/agentgateway/compare/v1.3.1...v1.4.0">v1.3.1&amp;hellip;v1.4.0&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Install bits&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Images: &lt;code>cr.agentgateway.dev/agentgateway:v1.4.0&lt;/code>, &lt;code>cr.agentgateway.dev/controller:v1.4.0&lt;/code> (glibc — &lt;strong>no more musl image tags&lt;/strong>)&lt;/li>
&lt;li>Charts: &lt;code>agentgateway&lt;/code>, &lt;code>agentgateway-crds&lt;/code>, &lt;code>agentgateway-standalone&lt;/code>&lt;/li>
&lt;li>Binaries: still built with musl; proxy + &lt;code>agctl&lt;/code>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="the-short-version">The short version&lt;/h2>
&lt;p>Upstream’s own highlight reel:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Full MCP &lt;code>2026-07-28&lt;/code> support&lt;/strong> (and a year of community work behind it)&lt;/li>
&lt;li>&lt;strong>Cross App Access / Enterprise-Managed Authorization for MCP&lt;/strong> (ID-JAG)&lt;/li>
&lt;li>&lt;strong>OAuth token exchange&lt;/strong> backend auth (RFC 8693 + JWT bearer)&lt;/li>
&lt;li>&lt;strong>Standalone &lt;code>gateways&lt;/code>&lt;/strong> + DB-backed UI config (sqlite / postgres) + &lt;strong>standalone Helm chart&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Experimental &lt;code>AgentgatewayModel&lt;/code>&lt;/strong> on Kubernetes (off by default)&lt;/li>
&lt;li>Gateway API &lt;strong>v1.6&lt;/strong>, fault injection, richer guardrails, lots of fixes&lt;/li>
&lt;/ol>
&lt;p>My add-on for operators: &lt;strong>read the breaking + security sections before you helm upgrade.&lt;/strong> There is a High severity MCP auth advisory in this release.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Area&lt;/th>
&lt;th>What moved&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>MCP&lt;/td>
&lt;td>Protocol 2026-07-28, Apps, traces via &lt;code>_meta&lt;/code>, Entra/Descope/authentik&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Auth&lt;/td>
&lt;td>Token exchange, Cross App Access (incl. MCP EMA), hashed API keys&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Standalone&lt;/td>
&lt;td>&lt;code>gateways&lt;/code> supersede &lt;code>binds&lt;/code>, UI on same port, sqlite/postgres storage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>UI&lt;/td>
&lt;td>Settings to bind console + attach OIDC/JWT/API key/CSRF/CORS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kubernetes&lt;/td>
&lt;td>Experimental &lt;code>AgentgatewayModel&lt;/code> (off by default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Platform&lt;/td>
&lt;td>DaemonSet, Gateway API 1.6 / TCPRoute v1, monitors, delay fault injection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Security&lt;/td>
&lt;td>&lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">GHSA-mvgg-jvj2-4frq&lt;/a> + CEL body guidance&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="read-this-first-breaking-changes">Read this first: breaking changes&lt;/h2>
&lt;p>Straight from upstream. Do these before you call the upgrade done.&lt;/p>
&lt;h3 id="1-gateway-api-v16-and-tcproute-v1">1. Gateway API v1.6 and TCPRoute v1&lt;/h3>
&lt;p>agentgateway builds against &lt;strong>Gateway API v1.6&lt;/strong>. The controller uses &lt;strong>&lt;code>TCPRoute&lt;/code> v1&lt;/strong>, not &lt;code>v1alpha2&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> re-apply the Gateway API CRDs that match this release &lt;strong>before&lt;/strong> you upgrade the controller/proxy.&lt;/p>
&lt;h3 id="2-mcp-request-phase-guardrail-rejections-return-http-200">2. MCP request-phase guardrail rejections return HTTP 200&lt;/h3>
&lt;p>If an MCP guardrail rejects in the &lt;strong>request&lt;/strong> phase, the gateway now returns &lt;strong>HTTP 200&lt;/strong> with a JSON-RPC error body — same as the response phase.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> fix clients/tests that expected a non-200 on request-phase rejects.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/mcp/guardrails/">k8s MCP guardrails&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/mcp/guardrails/">standalone&lt;/a>&lt;/p>
&lt;h3 id="3-standalone-authlocation-no-longer-nests-expression">3. Standalone &lt;code>auth.location&lt;/code> no longer nests &lt;code>expression&lt;/code>&lt;/h3>
&lt;p>Custom token location config dropped the double-nested &lt;code>expression&lt;/code> field. Use the flattened form.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> grep your standalone policies for nested &lt;code>auth.location&lt;/code> expressions and update them.&lt;/p>
&lt;h3 id="4-musl-container-images-removed">4. musl container images removed&lt;/h3>
&lt;p>Musl &lt;strong>image&lt;/strong> variants are gone. Use standard glibc images. &lt;strong>Binary&lt;/strong> releases are still musl builds.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> update image pins, digests, and SBOMs.&lt;/p>
&lt;hr>
&lt;h2 id="read-this-second-security">Read this second: security&lt;/h2>
&lt;h3 id="high-mcp-sessions-crossing-routes-ghsa-mvgg-jvj2-4frq">High: MCP sessions crossing routes (GHSA-mvgg-jvj2-4frq)&lt;/h3>
&lt;p>This release fixes &lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">GHSA-mvgg-jvj2-4frq&lt;/a> — &lt;strong>High (8.1)&lt;/strong> — where stateful MCP sessions could cross routes and overwrite authorization policy.&lt;/p>
&lt;p>Read the advisory for impact and mitigation. Credit: &lt;a href="https://github.com/0dd">Aonan Guan&lt;/a>.&lt;/p>
&lt;p>Related hardening in the same train includes pinning MCP sessions to a backend and tightening passthrough checks. Still: if you run MCP with authz on 1.3, treat 1.4 as a &lt;strong>security upgrade&lt;/strong>, not just a feature bump.&lt;/p>
&lt;h3 id="cel-requestbody--responsebody-in-auth-policies">CEL &lt;code>request.body&lt;/code> / &lt;code>response.body&lt;/code> in auth policies&lt;/h3>
&lt;p>Upstream flags a footgun (recommendation + behavior change), also from the same reporter.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>1.3.x and earlier&lt;/th>
&lt;th>1.4&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>request.body&lt;/code>&lt;/td>
&lt;td>Truncated to &lt;code>http.maxBufferSize&lt;/code> (default 2MB)&lt;/td>
&lt;td>&lt;strong>Not available if truncated&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>request.truncatedBody&lt;/code>&lt;/td>
&lt;td>Not available&lt;/td>
&lt;td>Truncated to max buffer size&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Why it matters: a policy like &lt;code>string(request.truncatedBody).contains(&amp;quot;attacker-payload&amp;quot;)&lt;/code> can miss a match when the body is over 2MB, or when encoding/compression changes what you thought you were matching. Auth on raw bodies is easy to get wrong.&lt;/p>
&lt;p>&lt;strong>Action:&lt;/strong> audit CEL auth that touches bodies. Prefer not using body contents for hard authorization when you can avoid it. If you must, handle the “body missing / truncated” case explicitly.&lt;/p>
&lt;hr>
&lt;h2 id="what-13-already-gave-you">What 1.3 already gave you&lt;/h2>
&lt;p>v1.3.0 brought the rebuilt UI (LLM / MCP / Traffic), cost tracking, virtual models, reusable providers/guardrails, more LLM providers, body buffering, better CEL/&lt;code>agctl&lt;/code>, better traces.&lt;/p>
&lt;p>v1.3.1 was polish: controller &lt;code>podLabels&lt;/code>, Vertex multi-region, same port with different protocols across gateways, UI wildcard/virtual-model fixes.&lt;/p>
&lt;p>1.3.1 does not force a redesign. &lt;strong>1.4 does&lt;/strong> — config shape, CRDs, MCP auth behavior, and at least one security-fixing upgrade path.&lt;/p>
&lt;hr>
&lt;h2 id="1-mcp-protocol-2026-07-28">1. MCP protocol 2026-07-28&lt;/h2>
&lt;p>This is highlight #1 in the official notes.&lt;/p>
&lt;p>agentgateway adds support for the upcoming &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/">MCP &lt;code>2026-07-28&lt;/code> protocol&lt;/a>:&lt;/p>
&lt;ul>
&lt;li>Stateful &lt;strong>and&lt;/strong> stateless servers (SEP-2575 server-stateless gap closed; skip synthetic &lt;code>initialize&lt;/code> for modern requests)&lt;/li>
&lt;li>Trace context through MCP &lt;strong>&lt;code>_meta&lt;/code>&lt;/strong> (SEP-414 / &lt;code>traceparent&lt;/code>) — stdio backends stay on the trace&lt;/li>
&lt;li>Basic &lt;strong>MCP Apps&lt;/strong> (plus multiplexing fixes for app-originated tool calls)&lt;/li>
&lt;li>Multi-target subscriptions/listen, opaque URI multiplexing, preserved multi-resource tool-result capabilities&lt;/li>
&lt;/ul>
&lt;p>Most of this is new enough that dedicated guides are still thin. Use the &lt;a href="https://agentgateway.dev/docs/kubernetes/main/reference/api/">Kubernetes API reference&lt;/a> and &lt;a href="https://agentgateway.dev/docs/standalone/main/reference/configuration/">Standalone config reference&lt;/a> for fields available today.&lt;/p>
&lt;h3 id="new-mcp-identity-providers">New MCP identity providers&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Microsoft Entra ID&lt;/strong> (k8s + standalone) — bridges Entra vs MCP-auth mismatches (OIDC discovery metadata, strip RFC 8707 &lt;code>resource&lt;/code>, short-circuit DCR with your pre-registered app id)&lt;/li>
&lt;li>&lt;strong>Descope&lt;/strong> and &lt;strong>authentik&lt;/strong> on standalone&lt;/li>
&lt;li>Okta was already first-class in 1.3&lt;/li>
&lt;/ul>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/mcp/auth/">k8s MCP auth&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/mcp/mcp-authn/">standalone MCP auth&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/integrations/auth/descope/">Descope guide&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="2-cross-app-access-for-mcp-and-backends">2. Cross App Access (for MCP and backends)&lt;/h2>
&lt;p>Official name in the MCP world: &lt;a href="https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization">Enterprise-Managed Authorization&lt;/a>. Implementation path: OAuth Identity Assertion Authorization Grant — &lt;strong>Cross App Access / ID-JAG&lt;/strong>.&lt;/p>
&lt;p>An enterprise IdP can broker access between a client app and an MCP server (or other backend) without a separate interactive OAuth dance for every downstream app.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/security/backend-authn-cross-app-access/">k8s Cross App Access&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/configuration/security/backend-authn/cross-app-access/">standalone&lt;/a>&lt;/p>
&lt;p>Examples landed for Keycloak, xaa.dev, token exchange, and JWT bearer. Subject token source is configurable.&lt;/p>
&lt;hr>
&lt;h2 id="3-oauth-token-exchange-backend-auth">3. OAuth token exchange backend auth&lt;/h2>
&lt;p>The gateway can exchange an incoming token for a backend credential using &lt;a href="https://datatracker.ietf.org/doc/html/rfc8693">RFC 8693&lt;/a> token exchange and &lt;a href="https://datatracker.ietf.org/doc/html/rfc7523">RFC 7523&lt;/a> JWT bearer.&lt;/p>
&lt;p>Also in this release:&lt;/p>
&lt;ul>
&lt;li>Kubernetes controller support&lt;/li>
&lt;li>Custom token types + OAuth 2.1-ish exchange defaults&lt;/li>
&lt;li>Inject &lt;strong>multiple&lt;/strong> secret-sourced headers&lt;/li>
&lt;li>Override the resolved secret key for OAuth/GCP-style refs&lt;/li>
&lt;/ul>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/security/backend-authn-oauth/">k8s OAuth token exchange&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/configuration/security/backend-authn/oauth-token-exchange/">standalone&lt;/a>&lt;/p>
&lt;p>If your 1.3 setup is “edge JWT ok, upstream still sees a shared service account,” this is the fix.&lt;/p>
&lt;h3 id="related-auth-extras">Related auth extras&lt;/h3>
&lt;ul>
&lt;li>Virtual keys from &lt;strong>ConfigMaps&lt;/strong> (not only Secrets)&lt;/li>
&lt;li>API keys stored as &lt;strong>SHA-256 hashes&lt;/strong> (raw key need not live in plaintext config)&lt;/li>
&lt;li>Admin IP allowlist + timing-attack fixes&lt;/li>
&lt;li>&lt;code>private_key_jwt&lt;/code> with certificate credentials&lt;/li>
&lt;li>AWS assume-role: session tags + &lt;code>RoleSessionName&lt;/code> / &lt;code>sessionNameExpression&lt;/code> via CEL (e.g. propagate &lt;code>jwt.sub&lt;/code>)&lt;/li>
&lt;li>Per-backend SigV4 region&lt;/li>
&lt;li>Frontend TLS with &lt;strong>multiple client CAs&lt;/strong>&lt;/li>
&lt;li>Bundled certs&lt;/li>
&lt;li>Cloud auth fetch timeouts&lt;/li>
&lt;/ul>
&lt;p>Virtual keys docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/llm/cost-controls/virtual-keys/">k8s&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/llm/cost-controls/virtual-keys/">standalone&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="4-standalone-gateways-storage-helm">4. Standalone: &lt;code>gateways&lt;/code>, storage, Helm&lt;/h2>
&lt;h3 id="gateways-replace-binds">&lt;code>gateways&lt;/code> replace &lt;code>binds&lt;/code>&lt;/h3>
&lt;p>New top-level &lt;strong>&lt;code>gateways&lt;/code>&lt;/strong> unifies UI, LLM, MCP, and ordinary routes on one listener/port. That is what makes &lt;strong>OIDC on the UI&lt;/strong> a first-class story.&lt;/p>
&lt;p>Important nuance from upstream:&lt;/p>
&lt;ul>
&lt;li>&lt;code>gateways&lt;/code> &lt;strong>supersedes&lt;/strong> &lt;code>binds&lt;/code>&lt;/li>
&lt;li>Existing &lt;strong>&lt;code>binds&lt;/code> still work&lt;/strong>&lt;/li>
&lt;li>The UI offers a &lt;strong>one-click migration&lt;/strong> from binds → gateways&lt;/li>
&lt;/ul>
&lt;p>Also: simpler host/TLS config, LLM+MCP on the same port, internal bind mode with wildcard fallback.&lt;/p>
&lt;p>Config reference: &lt;a href="https://agentgateway.dev/docs/standalone/main/reference/configuration/">standalone configuration&lt;/a>&lt;/p>
&lt;h3 id="database-backed-config-no-pvc-required">Database-backed config (no PVC required)&lt;/h3>
&lt;p>UI-created config can live in a database:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>sqlite&lt;/strong> for local&lt;/li>
&lt;li>&lt;strong>postgres&lt;/strong> for remote / HA&lt;/li>
&lt;/ul>
&lt;p>That avoids needing a persistent disk for writable UI workflows. Hybrid mode, policy writes, multi-replica notify, and “log errors to SQL too” landed in the same train. Storage naming moved away from the old configStore wording.&lt;/p>
&lt;p>Admin interface is &lt;strong>not&lt;/strong> exposed by default.&lt;/p>
&lt;h3 id="standalone-helm-chart">Standalone Helm chart&lt;/h3>
&lt;p>&lt;code>cr.agentgateway.dev/charts/agentgateway-standalone:v1.4.0&lt;/code> is real: modes for file vs database, gateways-oriented defaults, OIDC secret support, metrics service + ServiceMonitor.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/standalone/main/deployment/helm/">Standalone Helm&lt;/a>&lt;/p>
&lt;p>Treat 1.4 as the first serious baseline for that chart, not a soft bump from early experiments.&lt;/p>
&lt;hr>
&lt;h2 id="5-ui-settings-protect-the-console">5. UI Settings: protect the console&lt;/h2>
&lt;p>In the product UI: &lt;strong>Tools → Settings → UI Settings&lt;/strong>.&lt;/p>
&lt;p>Bind the console to a traffic gateway, then attach the same policy toolkit you use elsewhere.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/ui-settings.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-27-agentgateway-1-4-oss-from-1-3/ui-settings.png" alt="agentgateway 1.4 UI Settings — Public UI gateway set to ui, with policy cards for OIDC, JWT, Authorization, External authz, Basic auth, API keys, CSRF, and CORS" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>You get:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Public UI gateway&lt;/strong> picker + view diff / save&lt;/li>
&lt;li>Policy cards: OIDC, JWT, Authorization, External authz, Basic auth, API keys, CSRF, CORS&lt;/li>
&lt;li>Expandable top-level policy YAML for the GitOps-minded&lt;/li>
&lt;/ul>
&lt;p>After upgrade: bind the UI to the gateway you actually expose, turn on OIDC or JWT, then put that listener on a real network.&lt;/p>
&lt;hr>
&lt;h2 id="6-experimental-agentgatewaymodel-off-by-default">6. Experimental &lt;code>AgentgatewayModel&lt;/code> (off by default)&lt;/h2>
&lt;p>Kubernetes headline — with the official caveats.&lt;/p>
&lt;p>&lt;strong>&lt;code>AgentgatewayModel&lt;/code>&lt;/strong> brings the standalone model-centric LLM experience to Kubernetes: serve multiple models on one gateway, route by model name in the request. Higher level than assembling &lt;code>AgentgatewayBackend&lt;/code> + HTTPRoute body matchers by hand.&lt;/p>
&lt;ul>
&lt;li>Short names: &lt;code>agmodel&lt;/code> / &lt;code>agentgatewaymodels.agentgateway.dev&lt;/code>&lt;/li>
&lt;li>&lt;strong>Experimental&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Off by default&lt;/strong>&lt;/li>
&lt;li>Existing APIs remain available&lt;/li>
&lt;/ul>
&lt;p>Why try it: one GitOps object per model/family, less HTTPRoute glue, k8s and standalone stop telling different stories.&lt;/p>
&lt;p>Why not force it day one: experimental. Pilot one non-prod model after the cluster is stable on 1.4.&lt;/p>
&lt;hr>
&lt;h2 id="7-guardrails-ext_proc-cel-fault-injection">7. Guardrails, ext_proc, CEL, fault injection&lt;/h2>
&lt;h3 id="guardrails">Guardrails&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;code>BackendConnectionPolicy&lt;/code>&lt;/strong> for guardrail callouts (TCP/TLS/HTTP/tunnel) on OpenAI moderation, Bedrock guardrails, Google Model Armor&lt;/li>
&lt;li>Default callout timeouts&lt;/li>
&lt;li>Clearer guardrail decisions in logs/UI&lt;/li>
&lt;li>ext_proc &lt;strong>&lt;code>failureMode&lt;/code>&lt;/strong> (fail-open / fail-closed)&lt;/li>
&lt;/ul>
&lt;h3 id="external-processing">External processing&lt;/h3>
&lt;p>Controller support for &lt;code>metadataContext&lt;/code>, &lt;code>requestAttributes&lt;/code>, &lt;code>responseAttributes&lt;/code>, plus &lt;code>failureMode&lt;/code>.&lt;/p>
&lt;h3 id="cel">CEL&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Custom CEL functions&lt;/strong> you can register for policies&lt;/li>
&lt;li>Opt-in &lt;strong>CEL filters for OTel spans&lt;/strong>&lt;/li>
&lt;li>CEL filters that decouple OTLP log fields from stdout&lt;/li>
&lt;li>&lt;code>source.connectHeaders&lt;/code> for inbound CONNECT headers&lt;/li>
&lt;li>Header transformation &lt;strong>replace&lt;/strong> mode via CEL&lt;/li>
&lt;li>Body handling split: &lt;code>body&lt;/code> vs &lt;code>truncatedBody&lt;/code> (see security section)&lt;/li>
&lt;/ul>
&lt;p>CEL refs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/reference/cel/">k8s&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/reference/cel/">standalone&lt;/a>&lt;/p>
&lt;h3 id="fault-injection-delay">Fault injection: &lt;code>delay&lt;/code>&lt;/h3>
&lt;p>New &lt;strong>delay&lt;/strong> traffic policy injects latency before forward. Duration string, CEL duration, or number-as-milliseconds. Injected delay counts against the request timeout.&lt;/p>
&lt;p>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/resiliency/fault-injection/">k8s fault injection&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/configuration/resiliency/fault-injection/">standalone&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="8-llm-a2a-and-provider-path-fixes">8. LLM, A2A, and provider path fixes&lt;/h2>
&lt;p>From the official “LLM gateway enhancements” plus the long PR train:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Frontend TLS&lt;/strong> multi-CA client cert validation&lt;/li>
&lt;li>&lt;strong>Bedrock:&lt;/strong> Responses→Bedrock images, 64-char tool name sanitizing, cache-write tokens in access logs&lt;/li>
&lt;li>&lt;strong>Gemini / Vertex:&lt;/strong> embeddings fix, native generateContent paths, detect-mode model/usage extraction&lt;/li>
&lt;li>&lt;strong>Azure AI Foundry:&lt;/strong> Anthropic endpoints; Responses/date-based API path fixes&lt;/li>
&lt;li>&lt;strong>A2A v1.0&lt;/strong> agent card format in URL rewriting&lt;/li>
&lt;li>Reasoning fixes on messages→completions; body decode before JSON parse&lt;/li>
&lt;li>Cost: Prometheus counter; catalog ConfigMap loading; virtual keys from ConfigMaps&lt;/li>
&lt;li>Telemetry: tool calls, A2A response metrics, richer dtrace&lt;/li>
&lt;li>Semantic routing examples (including Responses)&lt;/li>
&lt;/ul>
&lt;p>Providers: &lt;a href="https://agentgateway.dev/docs/kubernetes/main/llm/providers/">k8s&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/main/llm/providers/">standalone&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="9-deployment-and-ops">9. Deployment and ops&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>DaemonSet&lt;/strong> data plane (still defaults to Deployment)&lt;/li>
&lt;li>Helm &lt;strong>&lt;code>extraContainers&lt;/code>&lt;/strong> on control plane pods&lt;/li>
&lt;li>Proxy scrape via &lt;strong>PodMonitor&lt;/strong>; standalone metrics Service + &lt;strong>ServiceMonitor&lt;/strong>&lt;/li>
&lt;li>&lt;code>agentgateway_controller_build_info&lt;/code> metric&lt;/li>
&lt;li>Standalone SQL logging for success &lt;strong>and&lt;/strong> error, with faster writes&lt;/li>
&lt;li>Internal bind mode (annotation + wildcard fallback)&lt;/li>
&lt;li>Frontend policies targetable to a Gateway port&lt;/li>
&lt;li>Buffer: stream when body exceeds limit instead of always hard-failing&lt;/li>
&lt;li>ext_authz HTTP cache + custom HTTP bodies&lt;/li>
&lt;li>&lt;code>agctl&lt;/code> much smaller; some trace/config commands under proxy subcommand; nightly artifacts&lt;/li>
&lt;li>&lt;strong>s390x&lt;/strong> support&lt;/li>
&lt;li>Embedded UI build by default (&lt;code>UI=0&lt;/code> to opt out)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="how-i-would-upgrade">How I would upgrade&lt;/h2>
&lt;h3 id="kubernetes">Kubernetes&lt;/h3>
&lt;ol>
&lt;li>Re-apply &lt;strong>Gateway API CRDs&lt;/strong> for the v1.6 / TCPRoute v1 set this release expects&lt;/li>
&lt;li>Bump &lt;strong>&lt;code>agentgateway-crds&lt;/code>&lt;/strong> → v1.4.0&lt;/li>
&lt;li>Bump controller + proxy &lt;strong>together&lt;/strong>&lt;/li>
&lt;li>If you run authenticated MCP: read &lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">GHSA-mvgg-jvj2-4frq&lt;/a> and validate session/route authz behavior&lt;/li>
&lt;li>Smoke HTTPRoutes, MCP, JWT/ext_authz, traces, guardrails&lt;/li>
&lt;li>Audit CEL auth that uses &lt;code>request.body&lt;/code> / &lt;code>response.body&lt;/code>&lt;/li>
&lt;li>Only then pilot &lt;strong>one&lt;/strong> non-prod &lt;code>AgentgatewayModel&lt;/code>&lt;/li>
&lt;li>Keep Deployment unless you truly need DaemonSet&lt;/li>
&lt;/ol>
&lt;h3 id="standalone">Standalone&lt;/h3>
&lt;ol>
&lt;li>Snapshot config&lt;/li>
&lt;li>Note: &lt;code>binds&lt;/code> still work; migrate with UI one-click or convert to &lt;code>gateways&lt;/code> deliberately&lt;/li>
&lt;li>Flatten any nested &lt;code>auth.location&lt;/code> expressions&lt;/li>
&lt;li>Pick storage: &lt;strong>sqlite&lt;/strong> local vs &lt;strong>postgres&lt;/strong> HA (drop PVC assumptions from early charts)&lt;/li>
&lt;li>Open &lt;strong>UI Settings&lt;/strong>, bind console, enable OIDC/JWT&lt;/li>
&lt;li>Install/upgrade &lt;code>agentgateway-standalone&lt;/code> v1.4.0 after reading values&lt;/li>
&lt;/ol>
&lt;h3 id="auth-heavy-estates">Auth-heavy estates&lt;/h3>
&lt;ol>
&lt;li>Inventory upstreams still on shared service accounts&lt;/li>
&lt;li>Prototype token exchange on one API&lt;/li>
&lt;li>Prototype Cross App Access for one MCP or SaaS API&lt;/li>
&lt;li>Prefer native Entra MCP provider over home-grown resource-parameter proxies&lt;/li>
&lt;li>Prefer hashed virtual keys / ConfigMap sources where you can&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="what-i-would-turn-on-first">What I would turn on first&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Security fix path&lt;/strong> — upgrade MCP authz estates for the High advisory&lt;/li>
&lt;li>&lt;strong>Gateway API CRD re-apply + clean 1.4 roll&lt;/strong>&lt;/li>
&lt;li>&lt;strong>One gateway + UI Settings&lt;/strong> — console actually protected&lt;/li>
&lt;li>&lt;strong>Token exchange or XAA&lt;/strong> — user-scoped upstream tokens&lt;/li>
&lt;li>&lt;strong>MCP 2026-07-28 + &lt;code>_meta&lt;/code> traces&lt;/strong> — federation you can debug&lt;/li>
&lt;li>&lt;strong>Experimental AgentgatewayModel&lt;/strong> — one non-prod pilot, not a big-bang rewrite&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="links">Links&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.4.0">Official v1.4.0 release&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/security/advisories/GHSA-mvgg-jvj2-4frq">Security advisory GHSA-mvgg-jvj2-4frq&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway/compare/v1.3.1...v1.4.0">v1.3.1&amp;hellip;v1.4.0&lt;/a>&lt;/li>
&lt;li>Docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest/quickstart/">Kubernetes quick start&lt;/a> · &lt;a href="https://agentgateway.dev/docs/standalone/latest/quickstart/">Standalone quick start&lt;/a>&lt;/li>
&lt;li>On this site: &lt;a href="https://maniak.io/articles/2026-07-12-eight-principles-of-an-ai-gateway-agentgateway/">Eight principles of an AI gateway&lt;/a> · &lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-v1-why-it-matters/">Why the v1 line matters&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>Bottom line:&lt;/strong> 1.4 is not only features. It is &lt;strong>MCP protocol catch-up&lt;/strong>, &lt;strong>real delegated auth&lt;/strong> (token exchange + Cross App Access for MCP), &lt;strong>standalone that can run UI/LLM/MCP on one port with DB-backed config&lt;/strong>, an &lt;strong>experimental model API&lt;/strong> on Kubernetes, and a set of &lt;strong>breaking + security fixes you should not skip&lt;/strong>.&lt;/p>
&lt;p>Re-apply Gateway API CRDs. Bump charts. Read the MCP session advisory. Bind and lock down the UI. Then play with AgentgatewayModel — in that order.&lt;/p></content:encoded></item><item><title>Budgets You Can See: LLM Cost Governance in the Enterprise agentgateway UI</title><link>https://maniak.io/articles/2026-07-15-agentgateway-enterprise-budgets-cost-management-ui/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-15-agentgateway-enterprise-budgets-cost-management-ui/</guid><description>&lt;p>A couple of weeks ago I wrote up &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">AI budgets and hard spend limits in Enterprise
agentgateway&lt;/a>
— a day-one, YAML-and-&lt;code>curl&lt;/code> field report on the new &lt;code>EnterpriseAgentgatewayBudget&lt;/code>
CRD. That post answered &lt;em>how do I declare a budget and watch it trip?&lt;/em>&lt;/p>
&lt;p>This one answers the question the platform lead actually asks at standup: &lt;strong>which
teams are on track, and who&amp;rsquo;s blown their allocation?&lt;/strong> Because the same budgets
you declare in YAML now surface as a first-class view in the solo enterprise for
agentgateway console — under &lt;strong>Cost Management → Budgets&lt;/strong> — where spend is
something you &lt;em>read&lt;/em>, not something you &lt;code>kubectl get&lt;/code> and squint at.&lt;/p>
&lt;p>This is a tour of that experience. Everything below is a live lab: a management
cluster running solo enterprise for agentgateway, three teams (platform, sales,
marketing) with per-user and per-team token budgets, and enough traffic driven
through the gateway to push one of them over the edge on purpose.&lt;/p>
&lt;h2 id="the-budgets-tab-at-a-glance">The Budgets tab at a glance&lt;/h2>
&lt;p>Cost Management is where the gateway&amp;rsquo;s per-request cost telemetry rolls up. The
&lt;strong>Budgets&lt;/strong> tab lists every budget selected by an active gateway
budget-enforcement policy, with a one-line health readout for each.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-list.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-list.png" alt="solo enterprise for agentgateway Cost Management Budgets tab showing five budgets — budget-demo, cost-hierarchy, team-budget-marketing, team-budget-platform, and team-budget-sales — each with its scope, entry count, summary, and a usage bar. The summary chips at top read 5 Budgets, 4 Within budget, 1 Exceeding budget." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Three things are worth reading here:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The summary chips&lt;/strong> — &lt;code>5 Budgets&lt;/code>, &lt;code>4 Within budget&lt;/code>, &lt;code>1 Exceeding budget&lt;/code>.
This is the whole point of the view: a single glance tells you the fleet is
mostly healthy and exactly one budget needs attention. Click the red
&lt;strong>Exceeding budget&lt;/strong> chip and the list filters to just the offender.&lt;/li>
&lt;li>&lt;strong>The Summary column&lt;/strong> — &lt;code>TOKENS 500 / DAILY · AUDIT (+1 more)&lt;/code>. Each budget is
denominated in &lt;strong>tokens or USD&lt;/strong>, accrues over a &lt;strong>rolling window&lt;/strong> (here,
Daily), and carries an enforcement action — &lt;strong>Audit&lt;/strong> or &lt;strong>Block&lt;/strong>. The
&lt;code>(+1 more)&lt;/code> tells you the budget has multiple entries with their own limits.&lt;/li>
&lt;li>&lt;strong>The Usage bars&lt;/strong> — green when under, red when over. &lt;code>team-budget-platform&lt;/code> is
the one showing red; everyone else is comfortably in the green.&lt;/li>
&lt;/ul>
&lt;p>The scope column (&lt;code>mgmt-cluster/agentgateway-system&lt;/code>) tells you where each budget
lives — cluster and namespace — which matters once teams start keeping their own
Budget objects in their own namespaces.&lt;/p>
&lt;h2 id="anatomy-of-a-budget">Anatomy of a budget&lt;/h2>
&lt;p>Before drilling in, the mental model. A &lt;strong>budget&lt;/strong> is a named container scoped to
a target (a gateway or route), holding one or more &lt;strong>entries&lt;/strong>. Each entry is an
independent limit with four dimensions:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>What it means&lt;/th>
&lt;th>In the screenshots&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Subject&lt;/strong>&lt;/td>
&lt;td>Which requests the entry applies to — a &lt;code>user&lt;/code>, a &lt;code>group&lt;/code>, or unscoped&lt;/td>
&lt;td>&lt;code>user alice&lt;/code>, &lt;code>group platform&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Limit&lt;/strong>&lt;/td>
&lt;td>The cap, in tokens or USD&lt;/td>
&lt;td>&lt;code>200 tokens&lt;/code>, &lt;code>500 tokens&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Rolling window&lt;/strong>&lt;/td>
&lt;td>The trailing period spend ages out of&lt;/td>
&lt;td>&lt;code>Daily (24 hours)&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>When exceeded&lt;/strong>&lt;/td>
&lt;td>&lt;code>Audit&lt;/code> (observe) or &lt;code>Block&lt;/code> (429)&lt;/td>
&lt;td>&lt;code>Audit&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The budget&amp;rsquo;s &lt;strong>Total Usage&lt;/strong> is the roll-up across its entries. That lets you do
something genuinely useful: put a &lt;strong>per-team&lt;/strong> ceiling and a &lt;strong>per-user&lt;/strong> ceiling
in the &lt;em>same&lt;/em> budget, and watch both at once. A single heavy user can be over
their personal allocation while the team as a whole is still fine — and the UI
shows you both facts side by side.&lt;/p>
&lt;h2 id="drilling-in-team-budget-platform">Drilling in: &lt;code>team-budget-platform&lt;/code>&lt;/h2>
&lt;p>This is the budget flying the red flag. Open it and the story is immediate:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-platform.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-platform.png" alt="Detail view of the team-budget-platform budget. Total Usage reads 71% — 497 of 700 tokens. Two entries: platform-alice-tokens (subject user alice) at 200 of 200 tokens, 100%, marked Over budget with a red bar; and platform-team-tokens (subject group platform) at 297 of 500 tokens, 59%, marked On track with a green bar. Both use a Daily rolling window and the Audit action." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Two entries, two very different stories:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>platform-alice-tokens&lt;/code>&lt;/strong> — scoped to &lt;code>user alice&lt;/code>, capped at 200 tokens/day.
She&amp;rsquo;s at &lt;strong>200 of 200 — 100%, Over budget&lt;/strong>, the bar solid red. Alice has been
the heavy user today.&lt;/li>
&lt;li>&lt;strong>&lt;code>platform-team-tokens&lt;/code>&lt;/strong> — scoped to &lt;code>group platform&lt;/code>, capped at 500 tokens/day.
The team is at &lt;strong>297 of 500 — 59%, On track&lt;/strong>, comfortably green.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>Total Usage&lt;/strong> rolls both into &lt;code>71% — 497 of 700 tokens&lt;/code>. That&amp;rsquo;s the
per-user + per-team pattern doing exactly what it should: &lt;strong>alice individually
tripped her limit, but the platform team still has room.&lt;/strong> One noisy user hasn&amp;rsquo;t
consumed the whole team&amp;rsquo;s allocation, and you can see precisely where the pressure
is without cross-referencing anything.&lt;/p>
&lt;p>Because both entries are set to &lt;strong>Audit&lt;/strong>, nothing is blocked — alice&amp;rsquo;s requests
still succeed. Audit mode is &lt;em>visibility first&lt;/em>: the gateway records the overage,
paints it red, and lets traffic through so you can decide whether that limit is
right before you ever arm enforcement. (Flip &lt;code>onBudgetExceeded&lt;/code> to &lt;strong>Block&lt;/strong> and
that same 100% entry starts returning &lt;code>429&lt;/code>s instead.)&lt;/p>
&lt;p>Each entry also has a &lt;strong>View in dashboard&lt;/strong> link that pivots straight into the
filtered Cost Management dashboard for that subject — from &amp;ldquo;who&amp;rsquo;s over&amp;rdquo; to &amp;ldquo;what
exactly did they run&amp;rdquo; in one click.&lt;/p>
&lt;h2 id="what-healthy-looks-like-team-budget-sales">What healthy looks like: &lt;code>team-budget-sales&lt;/code>&lt;/h2>
&lt;p>For contrast, the sales budget is the picture of a team well within its means:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-sales.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-sales.png" alt="Detail view of the team-budget-sales budget. Total Usage reads 12% — 76 of 630 tokens. Two entries: sales-dave-tokens (subject user dave) at 38 of 180 tokens, 21%, On track; and sales-team-tokens (subject group sales) at 38 of 450 tokens, 8%, On track. Both use a Daily rolling window and the Audit action, with green usage bars." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Same shape — a per-user entry (&lt;code>sales-dave-tokens&lt;/code>, &lt;code>user dave&lt;/code>, 180 tokens) and
a per-team entry (&lt;code>sales-team-tokens&lt;/code>, &lt;code>group sales&lt;/code>, 450 tokens) — but every bar
is green. Total usage sits at &lt;strong>12% — 76 of 630 tokens&lt;/strong>. Dave&amp;rsquo;s at 21% of his
personal cap, the team at 8% of theirs. Nothing to see, which is precisely the
signal you want most of your teams to be sending.&lt;/p>
&lt;p>Line the two budgets up and the console is doing real FinOps work: &lt;strong>same
gateway, same route, different wallets&lt;/strong> — platform under pressure, sales idle —
each with its own per-user and per-team accounting, all readable in seconds.&lt;/p>
&lt;h2 id="the-status-vocabulary">The status vocabulary&lt;/h2>
&lt;p>Two layers of status, and it&amp;rsquo;s worth being precise about them:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Per entry:&lt;/strong> &lt;strong>On track&lt;/strong> (green) or &lt;strong>Over budget&lt;/strong> (red). This is about a
single limit — one user, one group.&lt;/li>
&lt;li>&lt;strong>Per fleet:&lt;/strong> the summary chips count budgets as &lt;strong>Within budget&lt;/strong> or
&lt;strong>Exceeding budget&lt;/strong>. A budget is &amp;ldquo;exceeding&amp;rdquo; the moment &lt;em>any&lt;/em> of its entries
is over. That&amp;rsquo;s why one over-limit alice entry tips the whole
&lt;code>team-budget-platform&lt;/code> budget — and the fleet counter — into the red.&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-exceeding.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-exceeding.png" alt="The Budgets tab filtered to exceeding budgets, showing the summary chips 5 Budgets, 4 Within budget, and 1 Exceeding budget highlighted in red, with team-budget-platform as the single result and its usage bars fully red." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And crucially, &lt;strong>status is independent of enforcement.&lt;/strong> Everything in this lab
runs in &lt;strong>Audit&lt;/strong> — so &amp;ldquo;Over budget&amp;rdquo; and &amp;ldquo;Exceeding budget&amp;rdquo; here mean &lt;em>recorded
and flagged&lt;/em>, not &lt;em>blocked&lt;/em>. That separation is the feature: you get the full
red-flag dashboard experience long before you ever cut anyone off, which is
exactly how you should roll budgets out.&lt;/p>
&lt;h2 id="the-yaml-behind-the-view">The YAML behind the view&lt;/h2>
&lt;p>The console is a lens, not a second source of truth. Every budget carries a
&lt;strong>Resource YAML&lt;/strong> panel showing the &lt;code>EnterpriseAgentgatewayBudget&lt;/code> object it
renders. For &lt;code>team-budget-platform&lt;/code>, that object is roughly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayBudget&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">team-budget-platform&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budgets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">platform-alice-tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alice &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-user entry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">platform-team-tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">platform &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-team entry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>subject&lt;/code> block is what makes the two entries different: one keys on the
&lt;code>user&lt;/code> dimension, the other on &lt;code>group&lt;/code>. Everything you saw in the drill-in —
the two bars, the two limits, the rolled-up total — is just this object,
rendered. Create a budget in the UI and you get this YAML; commit this YAML via
GitOps and you get the UI. Same object, two doors.&lt;/p>
&lt;h2 id="how-it-works-under-the-hood">How it works under the hood&lt;/h2>
&lt;p>A few mechanics tie the pretty bars to reality — covered in depth in the
&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">hands-on post&lt;/a>,
so here&amp;rsquo;s the short version:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Dimensions come from request identity.&lt;/strong> &lt;code>user&lt;/code> and &lt;code>group&lt;/code> values are
resolved per request from trusted identity — headers derived from JWT claims or
API-key metadata — so the gateway knows whose wallet to charge before it ever
forwards the call. An entry&amp;rsquo;s subject matches when every key/value it lists
matches the request&amp;rsquo;s resolved dimensions (&lt;code>&amp;quot;*&amp;quot;&lt;/code> matches any non-missing value;
omit &lt;code>subject&lt;/code> and the entry applies to every request on the target).&lt;/li>
&lt;li>&lt;strong>Windows are rolling, not calendar.&lt;/strong> A &lt;code>Daily&lt;/code> budget looks at the trailing
24 hours. Spend ages out continuously — a tripped budget recovers as old
requests fall off the window, rather than snapping back to zero at midnight.&lt;/li>
&lt;li>&lt;strong>USD budgets need a cost catalog.&lt;/strong> Token budgets count &lt;code>total_tokens&lt;/code>
(input + output). Dollar budgets multiply realized per-request cost — computed
from the model cost catalog — so the console can show you &lt;code>USD 5 / DAILY&lt;/code> just
as easily as &lt;code>TOKENS 500 / DAILY&lt;/code> (both appear in the budget list above).&lt;/li>
&lt;li>&lt;strong>Audit and Block are the two gears.&lt;/strong> Audit records and flags; Block returns
&lt;code>429&lt;/code> with the rolling window in the reset header. The whole rollout path is:
ship budgets in Audit, watch the console for a week, then arm Block only where
runaway spend would actually hurt.&lt;/li>
&lt;/ul>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>The CRD gave platform teams a way to &lt;em>declare&lt;/em> LLM spending limits. The Cost
Management UI gives them a way to &lt;em>govern&lt;/em> against those limits day to day —
per-team and per-user ceilings, rolling windows, and a red-means-attention
readout that a finance partner can look at without touching &lt;code>kubectl&lt;/code>.&lt;/p>
&lt;p>The scenario in these screenshots — platform straining while alice sits over her
personal cap, sales cruising at 12% — took nothing more than a handful of token
budgets with &lt;code>user&lt;/code> and &lt;code>group&lt;/code> subjects and some traffic to push through them.
Every budget still lives as YAML you can GitOps; the console just makes the
answer to &lt;em>&amp;ldquo;who&amp;rsquo;s over?&amp;rdquo;&lt;/em> a one-glance question instead of a query.&lt;/p>
&lt;p>Audit first, watch the bars, then arm Block where it counts.&lt;/p></description><content:encoded>&lt;p>A couple of weeks ago I wrote up &lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">AI budgets and hard spend limits in Enterprise
agentgateway&lt;/a>
— a day-one, YAML-and-&lt;code>curl&lt;/code> field report on the new &lt;code>EnterpriseAgentgatewayBudget&lt;/code>
CRD. That post answered &lt;em>how do I declare a budget and watch it trip?&lt;/em>&lt;/p>
&lt;p>This one answers the question the platform lead actually asks at standup: &lt;strong>which
teams are on track, and who&amp;rsquo;s blown their allocation?&lt;/strong> Because the same budgets
you declare in YAML now surface as a first-class view in the solo enterprise for
agentgateway console — under &lt;strong>Cost Management → Budgets&lt;/strong> — where spend is
something you &lt;em>read&lt;/em>, not something you &lt;code>kubectl get&lt;/code> and squint at.&lt;/p>
&lt;p>This is a tour of that experience. Everything below is a live lab: a management
cluster running solo enterprise for agentgateway, three teams (platform, sales,
marketing) with per-user and per-team token budgets, and enough traffic driven
through the gateway to push one of them over the edge on purpose.&lt;/p>
&lt;h2 id="the-budgets-tab-at-a-glance">The Budgets tab at a glance&lt;/h2>
&lt;p>Cost Management is where the gateway&amp;rsquo;s per-request cost telemetry rolls up. The
&lt;strong>Budgets&lt;/strong> tab lists every budget selected by an active gateway
budget-enforcement policy, with a one-line health readout for each.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-list.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-list.png" alt="solo enterprise for agentgateway Cost Management Budgets tab showing five budgets — budget-demo, cost-hierarchy, team-budget-marketing, team-budget-platform, and team-budget-sales — each with its scope, entry count, summary, and a usage bar. The summary chips at top read 5 Budgets, 4 Within budget, 1 Exceeding budget." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Three things are worth reading here:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The summary chips&lt;/strong> — &lt;code>5 Budgets&lt;/code>, &lt;code>4 Within budget&lt;/code>, &lt;code>1 Exceeding budget&lt;/code>.
This is the whole point of the view: a single glance tells you the fleet is
mostly healthy and exactly one budget needs attention. Click the red
&lt;strong>Exceeding budget&lt;/strong> chip and the list filters to just the offender.&lt;/li>
&lt;li>&lt;strong>The Summary column&lt;/strong> — &lt;code>TOKENS 500 / DAILY · AUDIT (+1 more)&lt;/code>. Each budget is
denominated in &lt;strong>tokens or USD&lt;/strong>, accrues over a &lt;strong>rolling window&lt;/strong> (here,
Daily), and carries an enforcement action — &lt;strong>Audit&lt;/strong> or &lt;strong>Block&lt;/strong>. The
&lt;code>(+1 more)&lt;/code> tells you the budget has multiple entries with their own limits.&lt;/li>
&lt;li>&lt;strong>The Usage bars&lt;/strong> — green when under, red when over. &lt;code>team-budget-platform&lt;/code> is
the one showing red; everyone else is comfortably in the green.&lt;/li>
&lt;/ul>
&lt;p>The scope column (&lt;code>mgmt-cluster/agentgateway-system&lt;/code>) tells you where each budget
lives — cluster and namespace — which matters once teams start keeping their own
Budget objects in their own namespaces.&lt;/p>
&lt;h2 id="anatomy-of-a-budget">Anatomy of a budget&lt;/h2>
&lt;p>Before drilling in, the mental model. A &lt;strong>budget&lt;/strong> is a named container scoped to
a target (a gateway or route), holding one or more &lt;strong>entries&lt;/strong>. Each entry is an
independent limit with four dimensions:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Field&lt;/th>
&lt;th>What it means&lt;/th>
&lt;th>In the screenshots&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Subject&lt;/strong>&lt;/td>
&lt;td>Which requests the entry applies to — a &lt;code>user&lt;/code>, a &lt;code>group&lt;/code>, or unscoped&lt;/td>
&lt;td>&lt;code>user alice&lt;/code>, &lt;code>group platform&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Limit&lt;/strong>&lt;/td>
&lt;td>The cap, in tokens or USD&lt;/td>
&lt;td>&lt;code>200 tokens&lt;/code>, &lt;code>500 tokens&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Rolling window&lt;/strong>&lt;/td>
&lt;td>The trailing period spend ages out of&lt;/td>
&lt;td>&lt;code>Daily (24 hours)&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>When exceeded&lt;/strong>&lt;/td>
&lt;td>&lt;code>Audit&lt;/code> (observe) or &lt;code>Block&lt;/code> (429)&lt;/td>
&lt;td>&lt;code>Audit&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The budget&amp;rsquo;s &lt;strong>Total Usage&lt;/strong> is the roll-up across its entries. That lets you do
something genuinely useful: put a &lt;strong>per-team&lt;/strong> ceiling and a &lt;strong>per-user&lt;/strong> ceiling
in the &lt;em>same&lt;/em> budget, and watch both at once. A single heavy user can be over
their personal allocation while the team as a whole is still fine — and the UI
shows you both facts side by side.&lt;/p>
&lt;h2 id="drilling-in-team-budget-platform">Drilling in: &lt;code>team-budget-platform&lt;/code>&lt;/h2>
&lt;p>This is the budget flying the red flag. Open it and the story is immediate:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-platform.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-platform.png" alt="Detail view of the team-budget-platform budget. Total Usage reads 71% — 497 of 700 tokens. Two entries: platform-alice-tokens (subject user alice) at 200 of 200 tokens, 100%, marked Over budget with a red bar; and platform-team-tokens (subject group platform) at 297 of 500 tokens, 59%, marked On track with a green bar. Both use a Daily rolling window and the Audit action." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Two entries, two very different stories:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>platform-alice-tokens&lt;/code>&lt;/strong> — scoped to &lt;code>user alice&lt;/code>, capped at 200 tokens/day.
She&amp;rsquo;s at &lt;strong>200 of 200 — 100%, Over budget&lt;/strong>, the bar solid red. Alice has been
the heavy user today.&lt;/li>
&lt;li>&lt;strong>&lt;code>platform-team-tokens&lt;/code>&lt;/strong> — scoped to &lt;code>group platform&lt;/code>, capped at 500 tokens/day.
The team is at &lt;strong>297 of 500 — 59%, On track&lt;/strong>, comfortably green.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>Total Usage&lt;/strong> rolls both into &lt;code>71% — 497 of 700 tokens&lt;/code>. That&amp;rsquo;s the
per-user + per-team pattern doing exactly what it should: &lt;strong>alice individually
tripped her limit, but the platform team still has room.&lt;/strong> One noisy user hasn&amp;rsquo;t
consumed the whole team&amp;rsquo;s allocation, and you can see precisely where the pressure
is without cross-referencing anything.&lt;/p>
&lt;p>Because both entries are set to &lt;strong>Audit&lt;/strong>, nothing is blocked — alice&amp;rsquo;s requests
still succeed. Audit mode is &lt;em>visibility first&lt;/em>: the gateway records the overage,
paints it red, and lets traffic through so you can decide whether that limit is
right before you ever arm enforcement. (Flip &lt;code>onBudgetExceeded&lt;/code> to &lt;strong>Block&lt;/strong> and
that same 100% entry starts returning &lt;code>429&lt;/code>s instead.)&lt;/p>
&lt;p>Each entry also has a &lt;strong>View in dashboard&lt;/strong> link that pivots straight into the
filtered Cost Management dashboard for that subject — from &amp;ldquo;who&amp;rsquo;s over&amp;rdquo; to &amp;ldquo;what
exactly did they run&amp;rdquo; in one click.&lt;/p>
&lt;h2 id="what-healthy-looks-like-team-budget-sales">What healthy looks like: &lt;code>team-budget-sales&lt;/code>&lt;/h2>
&lt;p>For contrast, the sales budget is the picture of a team well within its means:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-sales.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/team-budget-sales.png" alt="Detail view of the team-budget-sales budget. Total Usage reads 12% — 76 of 630 tokens. Two entries: sales-dave-tokens (subject user dave) at 38 of 180 tokens, 21%, On track; and sales-team-tokens (subject group sales) at 38 of 450 tokens, 8%, On track. Both use a Daily rolling window and the Audit action, with green usage bars." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Same shape — a per-user entry (&lt;code>sales-dave-tokens&lt;/code>, &lt;code>user dave&lt;/code>, 180 tokens) and
a per-team entry (&lt;code>sales-team-tokens&lt;/code>, &lt;code>group sales&lt;/code>, 450 tokens) — but every bar
is green. Total usage sits at &lt;strong>12% — 76 of 630 tokens&lt;/strong>. Dave&amp;rsquo;s at 21% of his
personal cap, the team at 8% of theirs. Nothing to see, which is precisely the
signal you want most of your teams to be sending.&lt;/p>
&lt;p>Line the two budgets up and the console is doing real FinOps work: &lt;strong>same
gateway, same route, different wallets&lt;/strong> — platform under pressure, sales idle —
each with its own per-user and per-team accounting, all readable in seconds.&lt;/p>
&lt;h2 id="the-status-vocabulary">The status vocabulary&lt;/h2>
&lt;p>Two layers of status, and it&amp;rsquo;s worth being precise about them:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Per entry:&lt;/strong> &lt;strong>On track&lt;/strong> (green) or &lt;strong>Over budget&lt;/strong> (red). This is about a
single limit — one user, one group.&lt;/li>
&lt;li>&lt;strong>Per fleet:&lt;/strong> the summary chips count budgets as &lt;strong>Within budget&lt;/strong> or
&lt;strong>Exceeding budget&lt;/strong>. A budget is &amp;ldquo;exceeding&amp;rdquo; the moment &lt;em>any&lt;/em> of its entries
is over. That&amp;rsquo;s why one over-limit alice entry tips the whole
&lt;code>team-budget-platform&lt;/code> budget — and the fleet counter — into the red.&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-exceeding.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-15-agentgateway-enterprise-budgets-ui/budgets-exceeding.png" alt="The Budgets tab filtered to exceeding budgets, showing the summary chips 5 Budgets, 4 Within budget, and 1 Exceeding budget highlighted in red, with team-budget-platform as the single result and its usage bars fully red." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And crucially, &lt;strong>status is independent of enforcement.&lt;/strong> Everything in this lab
runs in &lt;strong>Audit&lt;/strong> — so &amp;ldquo;Over budget&amp;rdquo; and &amp;ldquo;Exceeding budget&amp;rdquo; here mean &lt;em>recorded
and flagged&lt;/em>, not &lt;em>blocked&lt;/em>. That separation is the feature: you get the full
red-flag dashboard experience long before you ever cut anyone off, which is
exactly how you should roll budgets out.&lt;/p>
&lt;h2 id="the-yaml-behind-the-view">The YAML behind the view&lt;/h2>
&lt;p>The console is a lens, not a second source of truth. Every budget carries a
&lt;strong>Resource YAML&lt;/strong> panel showing the &lt;code>EnterpriseAgentgatewayBudget&lt;/code> object it
renders. For &lt;code>team-budget-platform&lt;/code>, that object is roughly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayBudget&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">team-budget-platform&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budgets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">platform-alice-tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">alice &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-user entry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">platform-team-tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">platform &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-team entry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>subject&lt;/code> block is what makes the two entries different: one keys on the
&lt;code>user&lt;/code> dimension, the other on &lt;code>group&lt;/code>. Everything you saw in the drill-in —
the two bars, the two limits, the rolled-up total — is just this object,
rendered. Create a budget in the UI and you get this YAML; commit this YAML via
GitOps and you get the UI. Same object, two doors.&lt;/p>
&lt;h2 id="how-it-works-under-the-hood">How it works under the hood&lt;/h2>
&lt;p>A few mechanics tie the pretty bars to reality — covered in depth in the
&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">hands-on post&lt;/a>,
so here&amp;rsquo;s the short version:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Dimensions come from request identity.&lt;/strong> &lt;code>user&lt;/code> and &lt;code>group&lt;/code> values are
resolved per request from trusted identity — headers derived from JWT claims or
API-key metadata — so the gateway knows whose wallet to charge before it ever
forwards the call. An entry&amp;rsquo;s subject matches when every key/value it lists
matches the request&amp;rsquo;s resolved dimensions (&lt;code>&amp;quot;*&amp;quot;&lt;/code> matches any non-missing value;
omit &lt;code>subject&lt;/code> and the entry applies to every request on the target).&lt;/li>
&lt;li>&lt;strong>Windows are rolling, not calendar.&lt;/strong> A &lt;code>Daily&lt;/code> budget looks at the trailing
24 hours. Spend ages out continuously — a tripped budget recovers as old
requests fall off the window, rather than snapping back to zero at midnight.&lt;/li>
&lt;li>&lt;strong>USD budgets need a cost catalog.&lt;/strong> Token budgets count &lt;code>total_tokens&lt;/code>
(input + output). Dollar budgets multiply realized per-request cost — computed
from the model cost catalog — so the console can show you &lt;code>USD 5 / DAILY&lt;/code> just
as easily as &lt;code>TOKENS 500 / DAILY&lt;/code> (both appear in the budget list above).&lt;/li>
&lt;li>&lt;strong>Audit and Block are the two gears.&lt;/strong> Audit records and flags; Block returns
&lt;code>429&lt;/code> with the rolling window in the reset header. The whole rollout path is:
ship budgets in Audit, watch the console for a week, then arm Block only where
runaway spend would actually hurt.&lt;/li>
&lt;/ul>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>The CRD gave platform teams a way to &lt;em>declare&lt;/em> LLM spending limits. The Cost
Management UI gives them a way to &lt;em>govern&lt;/em> against those limits day to day —
per-team and per-user ceilings, rolling windows, and a red-means-attention
readout that a finance partner can look at without touching &lt;code>kubectl&lt;/code>.&lt;/p>
&lt;p>The scenario in these screenshots — platform straining while alice sits over her
personal cap, sales cruising at 12% — took nothing more than a handful of token
budgets with &lt;code>user&lt;/code> and &lt;code>group&lt;/code> subjects and some traffic to push through them.
Every budget still lives as YAML you can GitOps; the console just makes the
answer to &lt;em>&amp;ldquo;who&amp;rsquo;s over?&amp;rdquo;&lt;/em> a one-glance question instead of a query.&lt;/p>
&lt;p>Audit first, watch the bars, then arm Block where it counts.&lt;/p></content:encoded></item><item><title>On-Behalf-Of, Explained: How agentgateway Swaps an Entra Token for a Microsoft Graph Token</title><link>https://maniak.io/articles/2026-07-15-entra-obo-agentgateway-microsoft-graph/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-15-entra-obo-agentgateway-microsoft-graph/</guid><description>&lt;p>There&amp;rsquo;s a moment in almost every enterprise integration where you have &lt;em>a&lt;/em> token, but not &lt;em>the&lt;/em> token. You&amp;rsquo;re holding a perfectly valid JWT that your identity provider signed — your user is authenticated, the claims are real — and yet the API you need to call rejects it with a flat &lt;code>401&lt;/code>. Nothing is wrong with the token. It&amp;rsquo;s just addressed to someone else.&lt;/p>
&lt;p>That&amp;rsquo;s the problem the &lt;strong>OAuth 2.0 On-Behalf-Of (OBO)&lt;/strong> flow was invented to solve, and it&amp;rsquo;s the story this article tells. We&amp;rsquo;ll use a small, real lab: an MCP client holding an Entra (Azure AD) token, an &lt;a href="https://agentgateway.dev">Enterprise agentgateway&lt;/a> acting as the broker, and Microsoft Graph as the downstream API. By the end you&amp;rsquo;ll understand &lt;em>why&lt;/em> OBO exists, &lt;em>how&lt;/em> the exchange actually works on the wire, and &lt;em>how to&lt;/em> configure the gateway to do it for you — all reproducible with a single script, &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/scripts/test-entra-obo.sh">&lt;code>test-entra-obo.sh&lt;/code>&lt;/a>.&lt;/p>
&lt;h2 id="the-401-that-starts-everything">The 401 that starts everything&lt;/h2>
&lt;p>Let&amp;rsquo;s begin with the failure, because the failure is what makes OBO make sense.&lt;/p>
&lt;p>Your MCP client signs in a user through your corporate IdP and receives an access token. It&amp;rsquo;s a bearer token — you&amp;rsquo;d be forgiven for assuming you can now take it and call any Microsoft API. So you try the obvious thing: send it straight to Graph.&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant C as MCP Client
 participant E as Microsoft Entra
 participant G as Microsoft Graph

 C-&amp;gt;&amp;gt;E: Sign in — get access token
 E--&amp;gt;&amp;gt;C: JWT (aud = your-backend-api)
 C-&amp;gt;&amp;gt;G: GET /v1.0/me&amp;lt;br/&amp;gt;Authorization: Bearer &amp;lt;that JWT&amp;gt;
 G--&amp;gt;&amp;gt;C: 401 InvalidAuthenticationToken
 Note over C,G: The token is valid — just not for Graph.
&lt;/div>
&lt;p>The token is cryptographically sound. Entra signed it. The user is who they say they are. But Graph looks at one field and stops reading: &lt;strong>&lt;code>aud&lt;/code>&lt;/strong>, the audience.&lt;/p>
&lt;h2 id="why-the-audience-matters-the-whole-story-in-one-claim">Why the audience matters (the whole story in one claim)&lt;/h2>
&lt;p>Every access token carries an audience claim that names &lt;em>who the token is for&lt;/em>. When Entra minted your token, you asked for a token scoped to &lt;strong>your own backend API&lt;/strong> — an app registration with the ID &lt;code>0c00f6d2-5587-469d-9d35-7360d878fddc&lt;/code>, in this lab called &lt;code>goose-solo-ui-backend&lt;/code>. So the token says, in effect:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;This bearer is authorized to talk to &lt;code>0c00f6d2-…&lt;/code>, on behalf of &lt;code>sebastian@maniak.io&lt;/code>.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Microsoft Graph&amp;rsquo;s audience is &lt;code>https://graph.microsoft.com&lt;/code>. When Graph receives a token whose &lt;code>aud&lt;/code> is your backend, it correctly refuses it — a token for one audience must &lt;strong>never&lt;/strong> be accepted by another. That rule isn&amp;rsquo;t bureaucratic; it&amp;rsquo;s the thing that stops a token you handed to one service from being replayed against a completely different one. Audience scoping is a security feature, and OBO is how you work &lt;em>with&lt;/em> it instead of against it.&lt;/p>
&lt;p>Here is the token the client actually holds, decoded (this is real output from the demo&amp;rsquo;s &lt;code>token&lt;/code> mode):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">iss : https://login.microsoftonline.com/8635e970-…/v2.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aud : 0c00f6d2-5587-469d-9d35-7360d878fddc ← your backend, NOT Graph
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sub/oid : ed1b752d-d99c-421f-afc3-a6ebc323cd27
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user : sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">scp : access_as_user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">azp : 04b07795-8ddb-461a-bbee-02f9e1bf7b46
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The star of the show is &lt;code>aud&lt;/code>. It points at the middle tier. Graph would look at it and hand back &lt;code>InvalidAuthenticationToken&lt;/code>. &lt;strong>We need a token with &lt;code>aud = https://graph.microsoft.com&lt;/code> — but we need it to still represent the same signed-in user.&lt;/strong> That is precisely what OBO produces.&lt;/p>
&lt;h2 id="what-on-behalf-of-actually-is">What On-Behalf-Of actually is&lt;/h2>
&lt;p>OBO is a &lt;em>token exchange&lt;/em>. A middle-tier service (the &amp;ldquo;confused deputy&amp;rdquo; in older literature, here our gateway) takes the user&amp;rsquo;s token, presents it back to the identity provider as proof, and asks: &lt;em>&amp;ldquo;The user already authorized me. Now mint me a new token — for a different downstream API — that still acts on this same user&amp;rsquo;s behalf.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The IdP validates the incoming token, checks that the middle tier is allowed to request the downstream scope, and issues a brand-new token with the &lt;strong>downstream audience&lt;/strong> and the &lt;strong>same user identity&lt;/strong>. The user never re-authenticates. The downstream API never sees the original token. And — the part that matters for MCP servers — &lt;strong>the thing that performs the exchange holds the client secret; the code that calls Graph does not.&lt;/strong>&lt;/p>
&lt;div class="mermaid">graph LR
 A[&amp;#34;Token IN&amp;lt;br/&amp;gt;aud = your-backend&amp;lt;br/&amp;gt;user = sebastian&amp;#34;] --&amp;gt;|OBO exchange| B[&amp;#34;Token OUT&amp;lt;br/&amp;gt;aud = graph.microsoft.com&amp;lt;br/&amp;gt;user = sebastian&amp;#34;]
 style A fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style B fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>Same user. New audience. That single transformation is the entire point.&lt;/p>
&lt;h2 id="the-lab-who-is-who">The lab: who is who&lt;/h2>
&lt;p>Before the flow, meet the cast. The whole demo is three network hops wide.&lt;/p>
&lt;div class="mermaid">graph TD
 C[&amp;#34;MCP Client&amp;lt;br/&amp;gt;(test-entra-obo.sh / MCP Inspector)&amp;#34;]
 G[&amp;#34;agentgateway&amp;lt;br/&amp;gt;http://172.16.10.155:30160/graph-me&amp;lt;br/&amp;gt;policy: entra-obo-policy&amp;#34;]
 M[&amp;#34;graph-me-mcp&amp;lt;br/&amp;gt;(in-cluster MCP server)&amp;#34;]
 GR[&amp;#34;Microsoft Graph&amp;lt;br/&amp;gt;GET /v1.0/me&amp;#34;]
 E[&amp;#34;Microsoft Entra&amp;lt;br/&amp;gt;/oauth2/v2.0/token&amp;#34;]

 C --&amp;gt;|&amp;#34;Bearer: middle-tier JWT&amp;lt;br/&amp;gt;aud = goose-solo-ui-backend&amp;#34;| G
 G --&amp;gt;|&amp;#34;OBO: grant_type=jwt-bearer&amp;lt;br/&amp;gt;on_behalf_of&amp;#34;| E
 E --&amp;gt;|&amp;#34;access token&amp;lt;br/&amp;gt;aud = graph.microsoft.com&amp;#34;| G
 G --&amp;gt;|&amp;#34;injects Graph Bearer upstream&amp;#34;| M
 M --&amp;gt;|&amp;#34;GET /me + injected Bearer&amp;#34;| GR

 style C fill:#FFF7D6,stroke:#17181C,color:#17181C
 style G fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style M fill:#F1EFE9,stroke:#17181C,color:#17181C
 style GR fill:#F1EFE9,stroke:#17181C,color:#17181C
 style E fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Actor&lt;/th>
&lt;th>Role&lt;/th>
&lt;th>What it holds&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>MCP client&lt;/strong>&lt;/td>
&lt;td>Starts the request&lt;/td>
&lt;td>A middle-tier Entra JWT (&lt;code>aud = goose-solo-ui-backend&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>agentgateway&lt;/strong>&lt;/td>
&lt;td>The broker&lt;/td>
&lt;td>The app &lt;strong>client secret&lt;/strong>, and the OBO policy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Microsoft Entra&lt;/strong>&lt;/td>
&lt;td>Security token service&lt;/td>
&lt;td>The signing keys; issues the exchanged token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>graph-me-mcp&lt;/strong>&lt;/td>
&lt;td>Downstream MCP server&lt;/td>
&lt;td>&lt;em>Nothing&lt;/em> — it receives an injected Graph token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Microsoft Graph&lt;/strong>&lt;/td>
&lt;td>The protected API&lt;/td>
&lt;td>Validates &lt;code>aud = https://graph.microsoft.com&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The crucial design property: the &lt;strong>client&lt;/strong> never holds a Graph token, and the &lt;strong>MCP server&lt;/strong> never holds the client secret. The gateway sits in the middle and is the only party that touches both.&lt;/p>
&lt;h2 id="the-full-flow-end-to-end">The full flow, end to end&lt;/h2>
&lt;p>Now the whole thing on one wire diagram. This is what happens on the very first MCP call.&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant C as MCP Client
 participant AG as agentgateway&amp;lt;br/&amp;gt;(entra-obo-policy)
 participant E as Microsoft Entra (STS)
 participant M as graph-me-mcp
 participant G as Microsoft Graph

 C-&amp;gt;&amp;gt;AG: POST /graph-me (MCP initialize)&amp;lt;br/&amp;gt;Authorization: Bearer &amp;lt;middle-tier JWT&amp;gt;
 AG-&amp;gt;&amp;gt;AG: Validate JWT (iss, aud, signature)
 AG-&amp;gt;&amp;gt;E: POST /oauth2/v2.0/token&amp;lt;br/&amp;gt;grant_type=jwt-bearer&amp;lt;br/&amp;gt;assertion=&amp;lt;user JWT&amp;gt;&amp;lt;br/&amp;gt;requested_token_use=on_behalf_of&amp;lt;br/&amp;gt;scope=Graph/User.Read
 E--&amp;gt;&amp;gt;AG: access_token (aud = graph.microsoft.com)
 Note over AG: Gateway now holds a Graph-scoped&amp;lt;br/&amp;gt;token for THIS user.
 AG-&amp;gt;&amp;gt;M: Forward MCP request&amp;lt;br/&amp;gt;+ inject Graph Bearer upstream
 M-&amp;gt;&amp;gt;G: GET /v1.0/me&amp;lt;br/&amp;gt;Authorization: Bearer &amp;lt;injected token&amp;gt;
 G--&amp;gt;&amp;gt;M: 200 — user profile JSON
 M--&amp;gt;&amp;gt;AG: MCP tool result
 AG--&amp;gt;&amp;gt;C: 200 OK (+ MCP session id)
&lt;/div>
&lt;p>Read it top to bottom and the story is complete: a token that Graph would reject goes in the top; a real Graph profile comes out the bottom; the exchange in the middle is the only thing that changed, and it happened inside the gateway.&lt;/p>
&lt;h2 id="how-to-do-it-the-gateway-configuration">How to do it: the gateway configuration&lt;/h2>
&lt;p>The behavior above is declarative. Three small YAML objects wire it up — a &lt;strong>policy&lt;/strong> that describes the exchange, a &lt;strong>backend&lt;/strong> that points at the MCP server, and a &lt;strong>route&lt;/strong> that binds a path to them. These mirror the config referenced by the demo (&lt;code>config/policies/…&lt;/code>, &lt;code>config/backends/…&lt;/code>, &lt;code>config/routes/…&lt;/code>).&lt;/p>
&lt;h3 id="1-the-obo-policy">1. The OBO policy&lt;/h3>
&lt;p>This is where the exchange lives. The key block is &lt;code>tokenExchange.entra&lt;/code>, and the key setting is &lt;code>mode: ExchangeOnly&lt;/code> — meaning &amp;ldquo;just do the OBO swap, no interactive elicitation UI.&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># config/policies/entra-obo-policy.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entra-obo-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenExchange&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entra&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExchangeOnly &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># do the OBO swap; no elicitation prompt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tenantId&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8635e970-2205-4189&lt;/span>-&lt;span class="l">bc77-77519ff5064f&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clientId&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">0c00f6d2-5587-469d-9d35-7360d878fddc &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the middle-tier app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clientSecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entra-obo-secret &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the app secret — lives ONLY here&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scope&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://graph.microsoft.com/User.Read &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># downstream audience+scope&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything security-sensitive is contained in this one object: the tenant, the app that&amp;rsquo;s allowed to perform the exchange, the secret that proves it, and the exact downstream scope being requested. Nothing downstream needs any of it.&lt;/p>
&lt;h3 id="2-the-mcp-backend">2. The MCP backend&lt;/h3>
&lt;p>The backend declares the in-cluster MCP server that will receive the injected token.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># config/backends/graph-me-mcp.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp.default.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-the-route">3. The route&lt;/h3>
&lt;p>The route ties a public path to the backend and applies the policy. This is what makes &lt;code>POST /graph-me&lt;/code> do OBO.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># config/routes/graph-me-mcp-route.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/graph-me&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entra-obo-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the whole surface area. Add a path, attach a policy, point at a backend — the gateway handles the token dance.&lt;/p>
&lt;h2 id="watching-it-run-the-five-steps-of-the-demo">Watching it run: the five steps of the demo&lt;/h2>
&lt;p>The &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/scripts/test-entra-obo.sh">&lt;code>test-entra-obo.sh&lt;/code>&lt;/a> script narrates the exact flow above in five steps. Here&amp;rsquo;s what each one proves.&lt;/p>
&lt;h3 id="step-1--mint-the-middle-tier-token-what-the-client-holds">Step 1 — Mint the middle-tier token (what the client holds)&lt;/h3>
&lt;p>The script asks Entra for a token whose audience is the &lt;strong>middle-tier API&lt;/strong>, not Graph:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">az account get-access-token --resource api://0c00f6d2-5587-469d-9d35-7360d878fddc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">aud : 0c00f6d2-5587-469d-9d35-7360d878fddc ← middle-tier, not Graph
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user : sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">scp : access_as_user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">★ this token is MIDDLE-TIER scoped — Graph would 401 it.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Teaching point:&lt;/strong> if we sent this straight to Graph, we&amp;rsquo;d get the &lt;code>401&lt;/code> we opened with. OBO exists so the gateway can swap it first.&lt;/p>
&lt;h3 id="step-2--mcp-initialize-the-exchange-fires-here">Step 2 — MCP &lt;code>initialize&lt;/code> (the exchange fires here)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST http://172.16.10.155:30160/graph-me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Authorization: Bearer &amp;lt;middle-tier JWT&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ HTTP/1.1 200 OK (456 ms)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> server : graph-me-mcp v1.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> protocol : 2025-03-26
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> session : eyJ0IjoibWNwI… (send this on every later call)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On this call the gateway validates the JWT, calls Entra&amp;rsquo;s &lt;code>/oauth2/v2.0/token&lt;/code> with the OBO grant, and caches a Graph-scoped token for the upstream. The client sees only a normal MCP handshake and a session id.&lt;/p>
&lt;h3 id="step-3--toolslist">Step 3 — &lt;code>tools/list&lt;/code>&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">• graph_me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Call Microsoft Graph GET /me using the bearer token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> injected by agentgateway after the Entra OBO exchange.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Same middle-tier bearer from the client; the gateway silently reuses the exchanged Graph token upstream.&lt;/p>
&lt;h3 id="step-4--toolscall-graph_me-obo-in-action">Step 4 — &lt;code>tools/call graph_me&lt;/code> (OBO in action)&lt;/h3>
&lt;p>The MCP server&amp;rsquo;s code is almost embarrassingly simple — that&amp;rsquo;s the point:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">GET https://graph.microsoft.com/v1.0/me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Authorization: Bearer &amp;lt;token agentgateway injected&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Graph /me profile:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> displayName sebastian
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mail sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> userPrincipalName sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id ed1b752d-d99c-421f-afc3-a6ebc323cd27
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Real profile fields, returned by Graph, for the signed-in user. That could only happen if the upstream token&amp;rsquo;s &lt;code>aud&lt;/code> was &lt;code>https://graph.microsoft.com&lt;/code> — which proves the exchange worked.&lt;/p>
&lt;h3 id="step-5--summary">Step 5 — Summary&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">✓ OBO SUCCESS Graph /me → sebastian sebastian@maniak.io
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The client never held a Graph token. The MCP server never held the app secret. The gateway bridged the two.&lt;/p>
&lt;h2 id="where-obo-fits-among-the-other-patterns">Where OBO fits among the other patterns&lt;/h2>
&lt;p>This lab exposes three paths that look similar but solve different problems. Knowing which is which is half of understanding OBO.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>What it does&lt;/th>
&lt;th>Token exchange?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/graph-me&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Entra On-Behalf-Of — swap user token for a Graph token&lt;/td>
&lt;td>✅ Yes (this article)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/mcp-secure&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Entra JWT validation + tool-level RBAC only&lt;/td>
&lt;td>❌ No — same token forwarded&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/github-elicit&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Interactive elicitation / third-party OAuth store&lt;/td>
&lt;td>❌ No — user grants a &lt;em>new&lt;/em> consent&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The distinction that trips people up is &lt;code>/mcp-secure&lt;/code> vs &lt;code>/graph-me&lt;/code>. Both start from a valid Entra JWT. But &lt;code>/mcp-secure&lt;/code> just &lt;em>checks&lt;/em> the token and lets it through (fine when the downstream trusts the same audience). &lt;code>/graph-me&lt;/code> &lt;em>transforms&lt;/em> it — and you need that transformation precisely when the downstream audience differs, as Graph&amp;rsquo;s always will.&lt;/p>
&lt;h2 id="why-route-obo-through-a-gateway-at-all">Why route OBO through a gateway at all?&lt;/h2>
&lt;p>You could implement OBO inside every MCP server. The reason not to is the same reason you don&amp;rsquo;t put TLS termination or rate limiting in every service:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Secret containment.&lt;/strong> The client secret lives in exactly one place — the gateway&amp;rsquo;s policy. Your MCP servers become dumb HTTP callers that trust an injected header. If one is compromised, no exchange credential leaks with it.&lt;/li>
&lt;li>&lt;strong>Uniformity.&lt;/strong> Every backend that needs a Graph token gets it the same way, by attaching one policy. No per-service OAuth libraries, no drift.&lt;/li>
&lt;li>&lt;strong>Auditability.&lt;/strong> Every exchange happens at one chokepoint you can log, meter, and reason about.&lt;/li>
&lt;li>&lt;strong>Blast radius.&lt;/strong> Rotate the secret, change the scope, or revoke the whole path in one config object — not across a fleet of services.&lt;/li>
&lt;/ul>
&lt;p>The MCP server in this demo does one thing: &lt;code>GET /me&lt;/code> with whatever bearer it was handed. That simplicity is the &lt;em>product&lt;/em> of pushing OBO into the gateway.&lt;/p>
&lt;h2 id="run-it-yourself">Run it yourself&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># full narrated demo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./scripts/test-entra-obo.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># just decode and show the middle-tier token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./scripts/test-entra-obo.sh token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># quiet mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">QUIET&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> ./scripts/test-entra-obo.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-one-thing-to-remember">The one thing to remember&lt;/h2>
&lt;p>A valid token is not a universal token. Its audience pins it to one destination, on purpose. &lt;strong>On-Behalf-Of is the sanctioned way to cross that boundary&lt;/strong> — a middle tier proves the user already consented, and the IdP re-mints the identity for a new audience without a second login. Put that exchange in agentgateway and you get the best of both worlds: downstream services stay simple and secretless, while the one component that &lt;em>does&lt;/em> hold the secret is the one you can watch, rotate, and lock down.&lt;/p>
&lt;p>The client never sees Graph. The server never sees the secret. The gateway sees both, briefly, and only to trade one true token for another.&lt;/p></description><content:encoded>&lt;p>There&amp;rsquo;s a moment in almost every enterprise integration where you have &lt;em>a&lt;/em> token, but not &lt;em>the&lt;/em> token. You&amp;rsquo;re holding a perfectly valid JWT that your identity provider signed — your user is authenticated, the claims are real — and yet the API you need to call rejects it with a flat &lt;code>401&lt;/code>. Nothing is wrong with the token. It&amp;rsquo;s just addressed to someone else.&lt;/p>
&lt;p>That&amp;rsquo;s the problem the &lt;strong>OAuth 2.0 On-Behalf-Of (OBO)&lt;/strong> flow was invented to solve, and it&amp;rsquo;s the story this article tells. We&amp;rsquo;ll use a small, real lab: an MCP client holding an Entra (Azure AD) token, an &lt;a href="https://agentgateway.dev">Enterprise agentgateway&lt;/a> acting as the broker, and Microsoft Graph as the downstream API. By the end you&amp;rsquo;ll understand &lt;em>why&lt;/em> OBO exists, &lt;em>how&lt;/em> the exchange actually works on the wire, and &lt;em>how to&lt;/em> configure the gateway to do it for you — all reproducible with a single script, &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/scripts/test-entra-obo.sh">&lt;code>test-entra-obo.sh&lt;/code>&lt;/a>.&lt;/p>
&lt;h2 id="the-401-that-starts-everything">The 401 that starts everything&lt;/h2>
&lt;p>Let&amp;rsquo;s begin with the failure, because the failure is what makes OBO make sense.&lt;/p>
&lt;p>Your MCP client signs in a user through your corporate IdP and receives an access token. It&amp;rsquo;s a bearer token — you&amp;rsquo;d be forgiven for assuming you can now take it and call any Microsoft API. So you try the obvious thing: send it straight to Graph.&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant C as MCP Client
 participant E as Microsoft Entra
 participant G as Microsoft Graph

 C-&amp;gt;&amp;gt;E: Sign in — get access token
 E--&amp;gt;&amp;gt;C: JWT (aud = your-backend-api)
 C-&amp;gt;&amp;gt;G: GET /v1.0/me&amp;lt;br/&amp;gt;Authorization: Bearer &amp;lt;that JWT&amp;gt;
 G--&amp;gt;&amp;gt;C: 401 InvalidAuthenticationToken
 Note over C,G: The token is valid — just not for Graph.
&lt;/div>
&lt;p>The token is cryptographically sound. Entra signed it. The user is who they say they are. But Graph looks at one field and stops reading: &lt;strong>&lt;code>aud&lt;/code>&lt;/strong>, the audience.&lt;/p>
&lt;h2 id="why-the-audience-matters-the-whole-story-in-one-claim">Why the audience matters (the whole story in one claim)&lt;/h2>
&lt;p>Every access token carries an audience claim that names &lt;em>who the token is for&lt;/em>. When Entra minted your token, you asked for a token scoped to &lt;strong>your own backend API&lt;/strong> — an app registration with the ID &lt;code>0c00f6d2-5587-469d-9d35-7360d878fddc&lt;/code>, in this lab called &lt;code>goose-solo-ui-backend&lt;/code>. So the token says, in effect:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;This bearer is authorized to talk to &lt;code>0c00f6d2-…&lt;/code>, on behalf of &lt;code>sebastian@maniak.io&lt;/code>.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Microsoft Graph&amp;rsquo;s audience is &lt;code>https://graph.microsoft.com&lt;/code>. When Graph receives a token whose &lt;code>aud&lt;/code> is your backend, it correctly refuses it — a token for one audience must &lt;strong>never&lt;/strong> be accepted by another. That rule isn&amp;rsquo;t bureaucratic; it&amp;rsquo;s the thing that stops a token you handed to one service from being replayed against a completely different one. Audience scoping is a security feature, and OBO is how you work &lt;em>with&lt;/em> it instead of against it.&lt;/p>
&lt;p>Here is the token the client actually holds, decoded (this is real output from the demo&amp;rsquo;s &lt;code>token&lt;/code> mode):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">iss : https://login.microsoftonline.com/8635e970-…/v2.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aud : 0c00f6d2-5587-469d-9d35-7360d878fddc ← your backend, NOT Graph
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sub/oid : ed1b752d-d99c-421f-afc3-a6ebc323cd27
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user : sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">scp : access_as_user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">azp : 04b07795-8ddb-461a-bbee-02f9e1bf7b46
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The star of the show is &lt;code>aud&lt;/code>. It points at the middle tier. Graph would look at it and hand back &lt;code>InvalidAuthenticationToken&lt;/code>. &lt;strong>We need a token with &lt;code>aud = https://graph.microsoft.com&lt;/code> — but we need it to still represent the same signed-in user.&lt;/strong> That is precisely what OBO produces.&lt;/p>
&lt;h2 id="what-on-behalf-of-actually-is">What On-Behalf-Of actually is&lt;/h2>
&lt;p>OBO is a &lt;em>token exchange&lt;/em>. A middle-tier service (the &amp;ldquo;confused deputy&amp;rdquo; in older literature, here our gateway) takes the user&amp;rsquo;s token, presents it back to the identity provider as proof, and asks: &lt;em>&amp;ldquo;The user already authorized me. Now mint me a new token — for a different downstream API — that still acts on this same user&amp;rsquo;s behalf.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The IdP validates the incoming token, checks that the middle tier is allowed to request the downstream scope, and issues a brand-new token with the &lt;strong>downstream audience&lt;/strong> and the &lt;strong>same user identity&lt;/strong>. The user never re-authenticates. The downstream API never sees the original token. And — the part that matters for MCP servers — &lt;strong>the thing that performs the exchange holds the client secret; the code that calls Graph does not.&lt;/strong>&lt;/p>
&lt;div class="mermaid">graph LR
 A[&amp;#34;Token IN&amp;lt;br/&amp;gt;aud = your-backend&amp;lt;br/&amp;gt;user = sebastian&amp;#34;] --&amp;gt;|OBO exchange| B[&amp;#34;Token OUT&amp;lt;br/&amp;gt;aud = graph.microsoft.com&amp;lt;br/&amp;gt;user = sebastian&amp;#34;]
 style A fill:#FFF7D6,stroke:#E5341F,color:#17181C
 style B fill:#F1EFE9,stroke:#E5341F,color:#17181C
&lt;/div>
&lt;p>Same user. New audience. That single transformation is the entire point.&lt;/p>
&lt;h2 id="the-lab-who-is-who">The lab: who is who&lt;/h2>
&lt;p>Before the flow, meet the cast. The whole demo is three network hops wide.&lt;/p>
&lt;div class="mermaid">graph TD
 C[&amp;#34;MCP Client&amp;lt;br/&amp;gt;(test-entra-obo.sh / MCP Inspector)&amp;#34;]
 G[&amp;#34;agentgateway&amp;lt;br/&amp;gt;http://172.16.10.155:30160/graph-me&amp;lt;br/&amp;gt;policy: entra-obo-policy&amp;#34;]
 M[&amp;#34;graph-me-mcp&amp;lt;br/&amp;gt;(in-cluster MCP server)&amp;#34;]
 GR[&amp;#34;Microsoft Graph&amp;lt;br/&amp;gt;GET /v1.0/me&amp;#34;]
 E[&amp;#34;Microsoft Entra&amp;lt;br/&amp;gt;/oauth2/v2.0/token&amp;#34;]

 C --&amp;gt;|&amp;#34;Bearer: middle-tier JWT&amp;lt;br/&amp;gt;aud = goose-solo-ui-backend&amp;#34;| G
 G --&amp;gt;|&amp;#34;OBO: grant_type=jwt-bearer&amp;lt;br/&amp;gt;on_behalf_of&amp;#34;| E
 E --&amp;gt;|&amp;#34;access token&amp;lt;br/&amp;gt;aud = graph.microsoft.com&amp;#34;| G
 G --&amp;gt;|&amp;#34;injects Graph Bearer upstream&amp;#34;| M
 M --&amp;gt;|&amp;#34;GET /me + injected Bearer&amp;#34;| GR

 style C fill:#FFF7D6,stroke:#17181C,color:#17181C
 style G fill:#FFFFFF,stroke:#E5341F,color:#17181C
 style M fill:#F1EFE9,stroke:#17181C,color:#17181C
 style GR fill:#F1EFE9,stroke:#17181C,color:#17181C
 style E fill:#F1EFE9,stroke:#17181C,color:#17181C
&lt;/div>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Actor&lt;/th>
&lt;th>Role&lt;/th>
&lt;th>What it holds&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>MCP client&lt;/strong>&lt;/td>
&lt;td>Starts the request&lt;/td>
&lt;td>A middle-tier Entra JWT (&lt;code>aud = goose-solo-ui-backend&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>agentgateway&lt;/strong>&lt;/td>
&lt;td>The broker&lt;/td>
&lt;td>The app &lt;strong>client secret&lt;/strong>, and the OBO policy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Microsoft Entra&lt;/strong>&lt;/td>
&lt;td>Security token service&lt;/td>
&lt;td>The signing keys; issues the exchanged token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>graph-me-mcp&lt;/strong>&lt;/td>
&lt;td>Downstream MCP server&lt;/td>
&lt;td>&lt;em>Nothing&lt;/em> — it receives an injected Graph token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Microsoft Graph&lt;/strong>&lt;/td>
&lt;td>The protected API&lt;/td>
&lt;td>Validates &lt;code>aud = https://graph.microsoft.com&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The crucial design property: the &lt;strong>client&lt;/strong> never holds a Graph token, and the &lt;strong>MCP server&lt;/strong> never holds the client secret. The gateway sits in the middle and is the only party that touches both.&lt;/p>
&lt;h2 id="the-full-flow-end-to-end">The full flow, end to end&lt;/h2>
&lt;p>Now the whole thing on one wire diagram. This is what happens on the very first MCP call.&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant C as MCP Client
 participant AG as agentgateway&amp;lt;br/&amp;gt;(entra-obo-policy)
 participant E as Microsoft Entra (STS)
 participant M as graph-me-mcp
 participant G as Microsoft Graph

 C-&amp;gt;&amp;gt;AG: POST /graph-me (MCP initialize)&amp;lt;br/&amp;gt;Authorization: Bearer &amp;lt;middle-tier JWT&amp;gt;
 AG-&amp;gt;&amp;gt;AG: Validate JWT (iss, aud, signature)
 AG-&amp;gt;&amp;gt;E: POST /oauth2/v2.0/token&amp;lt;br/&amp;gt;grant_type=jwt-bearer&amp;lt;br/&amp;gt;assertion=&amp;lt;user JWT&amp;gt;&amp;lt;br/&amp;gt;requested_token_use=on_behalf_of&amp;lt;br/&amp;gt;scope=Graph/User.Read
 E--&amp;gt;&amp;gt;AG: access_token (aud = graph.microsoft.com)
 Note over AG: Gateway now holds a Graph-scoped&amp;lt;br/&amp;gt;token for THIS user.
 AG-&amp;gt;&amp;gt;M: Forward MCP request&amp;lt;br/&amp;gt;+ inject Graph Bearer upstream
 M-&amp;gt;&amp;gt;G: GET /v1.0/me&amp;lt;br/&amp;gt;Authorization: Bearer &amp;lt;injected token&amp;gt;
 G--&amp;gt;&amp;gt;M: 200 — user profile JSON
 M--&amp;gt;&amp;gt;AG: MCP tool result
 AG--&amp;gt;&amp;gt;C: 200 OK (+ MCP session id)
&lt;/div>
&lt;p>Read it top to bottom and the story is complete: a token that Graph would reject goes in the top; a real Graph profile comes out the bottom; the exchange in the middle is the only thing that changed, and it happened inside the gateway.&lt;/p>
&lt;h2 id="how-to-do-it-the-gateway-configuration">How to do it: the gateway configuration&lt;/h2>
&lt;p>The behavior above is declarative. Three small YAML objects wire it up — a &lt;strong>policy&lt;/strong> that describes the exchange, a &lt;strong>backend&lt;/strong> that points at the MCP server, and a &lt;strong>route&lt;/strong> that binds a path to them. These mirror the config referenced by the demo (&lt;code>config/policies/…&lt;/code>, &lt;code>config/backends/…&lt;/code>, &lt;code>config/routes/…&lt;/code>).&lt;/p>
&lt;h3 id="1-the-obo-policy">1. The OBO policy&lt;/h3>
&lt;p>This is where the exchange lives. The key block is &lt;code>tokenExchange.entra&lt;/code>, and the key setting is &lt;code>mode: ExchangeOnly&lt;/code> — meaning &amp;ldquo;just do the OBO swap, no interactive elicitation UI.&amp;rdquo;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># config/policies/entra-obo-policy.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entra-obo-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenExchange&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entra&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExchangeOnly &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># do the OBO swap; no elicitation prompt&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tenantId&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8635e970-2205-4189&lt;/span>-&lt;span class="l">bc77-77519ff5064f&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clientId&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">0c00f6d2-5587-469d-9d35-7360d878fddc &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the middle-tier app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">clientSecretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entra-obo-secret &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># the app secret — lives ONLY here&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scope&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://graph.microsoft.com/User.Read &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># downstream audience+scope&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything security-sensitive is contained in this one object: the tenant, the app that&amp;rsquo;s allowed to perform the exchange, the secret that proves it, and the exact downstream scope being requested. Nothing downstream needs any of it.&lt;/p>
&lt;h3 id="2-the-mcp-backend">2. The MCP backend&lt;/h3>
&lt;p>The backend declares the in-cluster MCP server that will receive the injected token.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># config/backends/graph-me-mcp.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp.default.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-the-route">3. The route&lt;/h3>
&lt;p>The route ties a public path to the backend and applies the policy. This is what makes &lt;code>POST /graph-me&lt;/code> do OBO.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># config/routes/graph-me-mcp-route.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/graph-me&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">filters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">entra-obo-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">graph-me-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the whole surface area. Add a path, attach a policy, point at a backend — the gateway handles the token dance.&lt;/p>
&lt;h2 id="watching-it-run-the-five-steps-of-the-demo">Watching it run: the five steps of the demo&lt;/h2>
&lt;p>The &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/scripts/test-entra-obo.sh">&lt;code>test-entra-obo.sh&lt;/code>&lt;/a> script narrates the exact flow above in five steps. Here&amp;rsquo;s what each one proves.&lt;/p>
&lt;h3 id="step-1--mint-the-middle-tier-token-what-the-client-holds">Step 1 — Mint the middle-tier token (what the client holds)&lt;/h3>
&lt;p>The script asks Entra for a token whose audience is the &lt;strong>middle-tier API&lt;/strong>, not Graph:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">az account get-access-token --resource api://0c00f6d2-5587-469d-9d35-7360d878fddc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">aud : 0c00f6d2-5587-469d-9d35-7360d878fddc ← middle-tier, not Graph
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">user : sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">scp : access_as_user
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">★ this token is MIDDLE-TIER scoped — Graph would 401 it.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Teaching point:&lt;/strong> if we sent this straight to Graph, we&amp;rsquo;d get the &lt;code>401&lt;/code> we opened with. OBO exists so the gateway can swap it first.&lt;/p>
&lt;h3 id="step-2--mcp-initialize-the-exchange-fires-here">Step 2 — MCP &lt;code>initialize&lt;/code> (the exchange fires here)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST http://172.16.10.155:30160/graph-me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Authorization: Bearer &amp;lt;middle-tier JWT&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">→ HTTP/1.1 200 OK (456 ms)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> server : graph-me-mcp v1.0.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> protocol : 2025-03-26
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> session : eyJ0IjoibWNwI… (send this on every later call)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On this call the gateway validates the JWT, calls Entra&amp;rsquo;s &lt;code>/oauth2/v2.0/token&lt;/code> with the OBO grant, and caches a Graph-scoped token for the upstream. The client sees only a normal MCP handshake and a session id.&lt;/p>
&lt;h3 id="step-3--toolslist">Step 3 — &lt;code>tools/list&lt;/code>&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">• graph_me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Call Microsoft Graph GET /me using the bearer token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> injected by agentgateway after the Entra OBO exchange.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Same middle-tier bearer from the client; the gateway silently reuses the exchanged Graph token upstream.&lt;/p>
&lt;h3 id="step-4--toolscall-graph_me-obo-in-action">Step 4 — &lt;code>tools/call graph_me&lt;/code> (OBO in action)&lt;/h3>
&lt;p>The MCP server&amp;rsquo;s code is almost embarrassingly simple — that&amp;rsquo;s the point:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">GET https://graph.microsoft.com/v1.0/me
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Authorization: Bearer &amp;lt;token agentgateway injected&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Graph /me profile:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> displayName sebastian
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> mail sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> userPrincipalName sebastian@maniak.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id ed1b752d-d99c-421f-afc3-a6ebc323cd27
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Real profile fields, returned by Graph, for the signed-in user. That could only happen if the upstream token&amp;rsquo;s &lt;code>aud&lt;/code> was &lt;code>https://graph.microsoft.com&lt;/code> — which proves the exchange worked.&lt;/p>
&lt;h3 id="step-5--summary">Step 5 — Summary&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">✓ OBO SUCCESS Graph /me → sebastian sebastian@maniak.io
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The client never held a Graph token. The MCP server never held the app secret. The gateway bridged the two.&lt;/p>
&lt;h2 id="where-obo-fits-among-the-other-patterns">Where OBO fits among the other patterns&lt;/h2>
&lt;p>This lab exposes three paths that look similar but solve different problems. Knowing which is which is half of understanding OBO.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>What it does&lt;/th>
&lt;th>Token exchange?&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/graph-me&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Entra On-Behalf-Of — swap user token for a Graph token&lt;/td>
&lt;td>✅ Yes (this article)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/mcp-secure&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Entra JWT validation + tool-level RBAC only&lt;/td>
&lt;td>❌ No — same token forwarded&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>/github-elicit&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Interactive elicitation / third-party OAuth store&lt;/td>
&lt;td>❌ No — user grants a &lt;em>new&lt;/em> consent&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The distinction that trips people up is &lt;code>/mcp-secure&lt;/code> vs &lt;code>/graph-me&lt;/code>. Both start from a valid Entra JWT. But &lt;code>/mcp-secure&lt;/code> just &lt;em>checks&lt;/em> the token and lets it through (fine when the downstream trusts the same audience). &lt;code>/graph-me&lt;/code> &lt;em>transforms&lt;/em> it — and you need that transformation precisely when the downstream audience differs, as Graph&amp;rsquo;s always will.&lt;/p>
&lt;h2 id="why-route-obo-through-a-gateway-at-all">Why route OBO through a gateway at all?&lt;/h2>
&lt;p>You could implement OBO inside every MCP server. The reason not to is the same reason you don&amp;rsquo;t put TLS termination or rate limiting in every service:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Secret containment.&lt;/strong> The client secret lives in exactly one place — the gateway&amp;rsquo;s policy. Your MCP servers become dumb HTTP callers that trust an injected header. If one is compromised, no exchange credential leaks with it.&lt;/li>
&lt;li>&lt;strong>Uniformity.&lt;/strong> Every backend that needs a Graph token gets it the same way, by attaching one policy. No per-service OAuth libraries, no drift.&lt;/li>
&lt;li>&lt;strong>Auditability.&lt;/strong> Every exchange happens at one chokepoint you can log, meter, and reason about.&lt;/li>
&lt;li>&lt;strong>Blast radius.&lt;/strong> Rotate the secret, change the scope, or revoke the whole path in one config object — not across a fleet of services.&lt;/li>
&lt;/ul>
&lt;p>The MCP server in this demo does one thing: &lt;code>GET /me&lt;/code> with whatever bearer it was handed. That simplicity is the &lt;em>product&lt;/em> of pushing OBO into the gateway.&lt;/p>
&lt;h2 id="run-it-yourself">Run it yourself&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># full narrated demo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./scripts/test-entra-obo.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># just decode and show the middle-tier token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./scripts/test-entra-obo.sh token
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># quiet mode&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">QUIET&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> ./scripts/test-entra-obo.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-one-thing-to-remember">The one thing to remember&lt;/h2>
&lt;p>A valid token is not a universal token. Its audience pins it to one destination, on purpose. &lt;strong>On-Behalf-Of is the sanctioned way to cross that boundary&lt;/strong> — a middle tier proves the user already consented, and the IdP re-mints the identity for a new audience without a second login. Put that exchange in agentgateway and you get the best of both worlds: downstream services stay simple and secretless, while the one component that &lt;em>does&lt;/em> hold the secret is the one you can watch, rotate, and lock down.&lt;/p>
&lt;p>The client never sees Graph. The server never sees the secret. The gateway sees both, briefly, and only to trade one true token for another.&lt;/p></content:encoded></item><item><title>Code Share: Deploy kagent with Agent Substrate</title><link>https://maniak.io/articles/2026-07-13-kagent-oss-agent-substrate-kind-guide/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-13-kagent-oss-agent-substrate-kind-guide/</guid><description>&lt;h1 id="code-share-deploy-kagent-with-agent-substrate">Code Share: Deploy kagent with Agent Substrate&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>Agent sessions are bursty. A user asks a question, the agent thinks for a few seconds, then the session sits idle for minutes — or hours — waiting on the next turn. Plain Kubernetes handles this badly: an idle pod still books its CPU and memory, and a cold pod takes seconds to come back. Multiply that across thousands of conversations and you&amp;rsquo;re paying for a lot of nothing.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://github.com/agent-substrate/substrate">Agent Substrate&lt;/a>&lt;/strong> flips the model. It decouples the &lt;em>agent session&lt;/em> from the &lt;em>pod&lt;/em>: idle sessions are checkpointed — full RAM and filesystem, via &lt;a href="https://gvisor.dev/">gVisor&lt;/a> — to object storage, the pod returns to a warm pool, and the session resumes &lt;strong>sub-second&lt;/strong> on the next request, exactly where it left off. Think serverless scale-to-zero, but for &lt;em>stateful&lt;/em> agents.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://kagent.dev">kagent&lt;/a>&lt;/strong> is the Kubernetes-native agent control plane. Wire substrate in as its execution layer and a declarative &lt;code>SandboxAgent&lt;/code> becomes a gVisor &lt;strong>actor&lt;/strong> instead of a long-running Deployment.&lt;/p>
&lt;p>This guide covers three things:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>What a substrate is&lt;/strong> (concepts from &lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a>)&lt;/li>
&lt;li>&lt;strong>What you can actually accomplish with it&lt;/strong> (use cases)&lt;/li>
&lt;li>&lt;strong>How to set it up&lt;/strong> with kagent OSS on a throwaway &lt;a href="https://kind.sigs.k8s.io/">kind&lt;/a> cluster — one-shot script or manual Helm&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>Runnable code for this path lives in &lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">&lt;code>01-kagent-agent-substrate&lt;/code>&lt;/a> (or your local kagent-demos clone). Prefer a guided lab? Same flow in the &lt;a href="https://github.com/sebbycorp/Instruqt-demos/tree/main/01-kagent-agent-substrate-workshop">Instruqt workshop&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="what-is-agent-substrate">What is Agent Substrate?&lt;/h2>
&lt;p>From the official atlas at &lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Agent Substrate&lt;/strong> is a Kubernetes-native runtime for highly-multiplexed actor workloads — AI agents, sandboxed environments, stateful services. It decouples actor lifecycle from Pods, so a small pool of pre-warmed gVisor workers can host &lt;strong>30× more actors than there are pods&lt;/strong>, by suspending idle actors to object storage and restoring them on demand.&lt;/p>
&lt;/blockquote>
&lt;p>Kubernetes is excellent at long-running services. It is not excellent at:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pain&lt;/th>
&lt;th>Why K8s struggles&lt;/th>
&lt;th>What substrate does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Idle agents&lt;/td>
&lt;td>Pods still consume CPU/memory&lt;/td>
&lt;td>Suspend to object storage; reclaim the worker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Millions of sessions&lt;/td>
&lt;td>API server / etcd not built for that QPS&lt;/td>
&lt;td>Actors live in Valkey/Redis, not as one CR per session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sub-second wake&lt;/td>
&lt;td>kube-scheduler + image pull is seconds&lt;/td>
&lt;td>Pre-warmed workers; resume bypasses the scheduler&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stateful scale-to-zero&lt;/td>
&lt;td>Volumes don&amp;rsquo;t attach/detach at agent speed&lt;/td>
&lt;td>Full RAM + filesystem checkpoint via gVisor&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Architecture bet in one sentence: &lt;strong>Kubernetes provisions infrastructure; substrate schedules actors.&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="core-concepts-glossary">Core concepts (glossary)&lt;/h2>
&lt;p>These terms show up everywhere in the UI, CRDs, and &lt;code>grpcurl&lt;/code> surface. Definitions match &lt;a href="https://learn.agentsubstrate.dev/concepts/actor/">learn.agentsubstrate.dev/concepts&lt;/a>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Term&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Actor&lt;/strong>&lt;/td>
&lt;td>One logical agent session. Has its own RAM, filesystem, and identity — but is &lt;strong>not pinned to a pod&lt;/strong>. Can suspend on worker A and resume on worker B.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Worker&lt;/strong>&lt;/td>
&lt;td>A pre-warmed pod that hosts &lt;strong>at most one&lt;/strong> actor at a time (&lt;code>IDLE&lt;/code> or assigned). Fungible hosting slot, not the agent itself.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>WorkerPool&lt;/strong>&lt;/td>
&lt;td>CRD for a Deployment of warm workers. You size concurrency by scaling the pool.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ActorTemplate&lt;/strong>&lt;/td>
&lt;td>Immutable “class” for actors (image, entrypoint, pool, snapshot location). Creating one builds a &lt;strong>golden snapshot&lt;/strong> (version 0).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden snapshot&lt;/strong>&lt;/td>
&lt;td>Fresh, just-booted image of the template. Brand-new actors restore from this instead of cold-booting.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Snapshot&lt;/strong>&lt;/td>
&lt;td>Checkpoint of RAM + sentry/filesystem state in S3/GCS (zstd). Resume uses demand paging so only touched pages load.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Suspend / Resume&lt;/strong>&lt;/td>
&lt;td>Checkpoint to object storage and free the worker / restore sub-second into an idle worker.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ateapi&lt;/strong>&lt;/td>
&lt;td>Control plane: actor lifecycle, worker assignment, suspend/resume workflows (gRPC).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>atenet&lt;/strong>&lt;/td>
&lt;td>L7 router + DNS. Per-request resolve of which worker hosts an actor; triggers resume if suspended.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>atelet&lt;/strong>&lt;/td>
&lt;td>Node DaemonSet: pulls images, downloads snapshots, talks to &lt;code>ateom-gvisor&lt;/code> over a Unix socket.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ateom-gvisor&lt;/strong>&lt;/td>
&lt;td>In-worker helper that shells out to &lt;code>runsc checkpoint&lt;/code> / &lt;code>runsc restore&lt;/code>.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="actor-lifecycle">Actor lifecycle&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">CreateActor → SUSPENDED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ResumeActor → RESUMING → RUNNING
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ SuspendActor → SUSPENDING → SUSPENDED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── DeleteActor (from SUSPENDED)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Only &lt;strong>RUNNING&lt;/strong> holds a worker. Between chat turns a declarative session is typically &lt;strong>SUSPENDED&lt;/strong>.&lt;/p>
&lt;h3 id="request-path-why-resume-is-fast">Request path (why resume is fast)&lt;/h3>
&lt;p>On each HTTP hit to an actor:&lt;/p>
&lt;ol>
&lt;li>DNS resolves &lt;code>actorId.actors.resources.substrate.ate.dev&lt;/code> → &lt;strong>atenet router&lt;/strong> (not the worker IP).&lt;/li>
&lt;li>ExtProc extracts the actor ID and calls &lt;strong>ateapi &lt;code>ResumeActor&lt;/code>&lt;/strong>.&lt;/li>
&lt;li>ateapi picks an idle worker from Redis (no kube-scheduler), asks &lt;strong>atelet&lt;/strong> to restore the snapshot.&lt;/li>
&lt;li>&lt;strong>ateom-gvisor&lt;/strong> runs &lt;code>runsc restore -background&lt;/code> — sentry comes up immediately; pages fault in on demand.&lt;/li>
&lt;li>Router rewrites &lt;code>:authority&lt;/code> to the worker pod and forwards the request.&lt;/li>
&lt;/ol>
&lt;p>Warm path (already RUNNING): skip restore, just route. Cold path (SUSPENDED): restore + route. No idle pod tax either way.&lt;/p>
&lt;p>Deep dive: &lt;a href="https://learn.agentsubstrate.dev/flows/resume-actor/">Resume actor end-to-end&lt;/a> · &lt;a href="https://learn.agentsubstrate.dev/topology/">System topology&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="what-can-you-accomplish-use-cases">What can you accomplish? (use cases)&lt;/h2>
&lt;p>Substrate is framework-agnostic at the OCI/gVisor layer. With &lt;strong>kagent OSS&lt;/strong> on top, these are the practical outcomes.&lt;/p>
&lt;h3 id="1-dense-multi-session-chat-agents-this-guide">1. Dense multi-session chat agents (this guide)&lt;/h3>
&lt;p>Many concurrent &lt;strong>declarative&lt;/strong> conversations without one Deployment per session.&lt;/p>
&lt;ul>
&lt;li>Each UI/chat session becomes a short-lived &lt;strong>actor&lt;/strong>.&lt;/li>
&lt;li>After the turn, the actor snapshots back; the worker serves the next session.&lt;/li>
&lt;li>One small WorkerPool multiplexes far more sessions than it has pods (~30× oversubscription is the design point).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>You get:&lt;/strong> serverless economics for stateful chat, with memory and filesystem preserved across turns.&lt;/p>
&lt;h3 id="2-sandboxed-cluster-assistants-kubernetes-tools-in-a-box">2. Sandboxed cluster assistants (Kubernetes tools in a box)&lt;/h3>
&lt;p>Run a &lt;code>SandboxAgent&lt;/code> with MCP tools (&lt;code>k8s_get_resources&lt;/code>, logs, events) &lt;strong>inside gVisor&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>Isolation: hostile or buggy tool use stays in the sandbox.&lt;/li>
&lt;li>Density: idle assistants don&amp;rsquo;t pin CPUs.&lt;/li>
&lt;li>Identity: actor is a first-class substrate identity, not “whatever ServiceAccount the pod had forever.”&lt;/li>
&lt;/ul>
&lt;p>This demo&amp;rsquo;s &lt;code>hello-substrate&lt;/code> is exactly that pattern — a Kubernetes assistant actor on the default pool.&lt;/p>
&lt;h3 id="3-coding-harnesses-and-long-lived-sandboxes-agentharness">3. Coding harnesses and long-lived sandboxes (AgentHarness)&lt;/h3>
&lt;p>kagent can place &lt;strong>OpenClaw / Hermes-style&lt;/strong> harnesses on substrate (&lt;code>runtime: substrate&lt;/code>).&lt;/p>
&lt;ul>
&lt;li>Shared actor for a coding session or workspace.&lt;/li>
&lt;li>Pins a worker while active (scale the pool: roughly &lt;code>1 + active harnesses&lt;/code> for headroom).&lt;/li>
&lt;li>Strong isolation for shell, git, and untrusted code.&lt;/li>
&lt;/ul>
&lt;p>See the &lt;a href="https://kagent.dev/docs/kagent/examples/agent-harness">kagent AgentHarness docs&lt;/a> after this walkthrough.&lt;/p>
&lt;h3 id="4-agent-swarms--many-actor-demos">4. Agent swarms / many-actor demos&lt;/h3>
&lt;p>Substrate&amp;rsquo;s own demos (e.g. many stateful counter actors on few workers) show the &lt;strong>multiplexing&lt;/strong> thesis: hundreds of stateful actors on a handful of pods.&lt;/p>
&lt;p>&lt;strong>You get:&lt;/strong> a path toward “millions of idle agents, thousands of wakeups/sec” without etcd holding every session.&lt;/p>
&lt;h3 id="5-security-minded-agent-platforms">5. Security-minded agent platforms&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>gVisor&lt;/strong> (or micro-VM class) per actor.&lt;/li>
&lt;li>Egress can sit behind &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> so agents never hold provider keys (Solo&amp;rsquo;s enterprise pattern).&lt;/li>
&lt;li>Snapshot-to-storage means you can reclaim compute without losing session state.&lt;/li>
&lt;/ul>
&lt;p>North-star metrics from the architecture docs: ~100 ms activation p95, extreme scale of idle actors, high wakeup throughput. Treat those as direction, not a SLA for the kind lab.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-on-kind">Architecture on kind&lt;/h2>
&lt;p>What you will run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────────────────── kind cluster (kagent-substrate) ────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ namespace: kagent namespace: ate-system │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────────────┐ ┌──────────────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-controller │ controller.substrate.* │ ate-api-server (scheduling) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-ui │ ───────────────────────▶│ atenet-router (L7 routing) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ SandboxAgent │ │ │ atelet (node supervisor) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ hello-substrate │ │ │ valkey-cluster (state) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ WorkerPool │ │ │ rustfs (snapshots/S3) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-default │ │ └──────────────────────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent Substrate&lt;/strong> (&lt;code>ate-system&lt;/code>)&lt;/td>
&lt;td>Multiplex actors onto warm workers; suspend/resume via gVisor + object storage.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong> (&lt;code>kagent&lt;/code>)&lt;/td>
&lt;td>Agent CRDs, UI, model provider (OpenAI); substrate as execution layer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Declarative agent → substrate &lt;strong>actor&lt;/strong> (not a long-running Deployment).&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="pinned-versions-this-guide">Pinned versions (this guide)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Version&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Agent Substrate&lt;/td>
&lt;td>&lt;code>0.0.8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kagent (OSS)&lt;/td>
&lt;td>&lt;code>0.10.0-beta6&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gVisor actor image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kind&lt;/td>
&lt;td>&lt;code>v0.31.0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Node image&lt;/td>
&lt;td>&lt;code>kindest/node:v1.35.0&lt;/code> (any &lt;strong>1.31+&lt;/strong> works)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Pairing matters.&lt;/strong> Substrate &lt;strong>0.0.8&lt;/strong> matches kagent &lt;strong>0.10.0-beta6&lt;/strong> ateapi/&lt;code>CreateActor&lt;/code> protos so UI chat works. Substrate &lt;strong>0.0.9&lt;/strong> broke that wire format — only bump together with a matching kagent.&lt;br>
&lt;strong>Node image matters.&lt;/strong> Substrate CRDs use CEL &lt;code>format.dns1123Label&lt;/code> / &lt;code>dns1123Subdomain&lt;/code> (Kubernetes &lt;strong>1.31+&lt;/strong>). Old kind defaults reject the CRDs.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Linux&lt;/strong> host or VM (~&lt;strong>8 vCPU / 16 GB RAM&lt;/strong>). Valkey (6) + control plane is heavy for a laptop-sized VM.&lt;/li>
&lt;li>Docker running&lt;/li>
&lt;li>OpenAI API key (real LLM calls)&lt;/li>
&lt;li>Optional: &lt;code>grpcurl&lt;/code> + &lt;code>jq&lt;/code> for the control-plane section&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>macOS / Windows:&lt;/strong> prefer a cloud Linux VM (&lt;code>n1-standard-8&lt;/code>, &lt;code>m5.2xlarge&lt;/code>, …). Docker Desktop often fails the gVisor checkpoint path.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="quick-start-one-shot-script">Quick start (one-shot script)&lt;/h2>
&lt;p>From the demo directory:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># optional: cp .env.example .env &amp;amp;&amp;amp; edit .env&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x setup.sh teardown.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>setup.sh&lt;/code> installs tools if missing, creates kind, installs substrate + kagent (wired), and applies &lt;code>manifests/hello-substrate.yaml&lt;/code>.&lt;/p>
&lt;p>Open the UI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward svc/kagent-ui 8080:8080
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># → http://localhost:8080 → Agents → hello-substrate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tear down:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./teardown.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="script-knobs">Script knobs&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Env var&lt;/th>
&lt;th>Default&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>OPENAI_API_KEY&lt;/code>&lt;/td>
&lt;td>&lt;em>(required)&lt;/em>&lt;/td>
&lt;td>Model provider key&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>KIND_CLUSTER&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-substrate&lt;/code>&lt;/td>
&lt;td>kind cluster name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SUBSTRATE_VERSION&lt;/code>&lt;/td>
&lt;td>&lt;code>0.0.8&lt;/code>&lt;/td>
&lt;td>Substrate chart&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>KAGENT_VERSION&lt;/code>&lt;/td>
&lt;td>&lt;code>0.10.0-beta6&lt;/code>&lt;/td>
&lt;td>kagent chart&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>WORKER_POOL_REPLICAS&lt;/code>&lt;/td>
&lt;td>&lt;code>2&lt;/code>&lt;/td>
&lt;td>Warm workers (2 helps golden-snapshot blue-green)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SKIP_TOOLS=1&lt;/code>&lt;/td>
&lt;td>off&lt;/td>
&lt;td>Don&amp;rsquo;t auto-install kubectl/helm/kind/…&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SKIP_CLUSTER=1&lt;/code>&lt;/td>
&lt;td>off&lt;/td>
&lt;td>Reuse existing cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SKIP_AGENT=1&lt;/code>&lt;/td>
&lt;td>off&lt;/td>
&lt;td>Skip applying the sample agent&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="manual-walkthrough-same-path-as-the-script">Manual walkthrough (same path as the script)&lt;/h2>
&lt;h3 id="env">Env&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="o">=&lt;/span>kagent-substrate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.0.8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.10.0-beta6
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>⚠️ Do &lt;strong>not&lt;/strong> write &lt;code>OPENAI_API_KEY=... helm ... --set ...=&amp;quot;${OPENAI_API_KEY}&amp;quot;&lt;/code> on one line — the variable expands &lt;em>before&lt;/em> the assignment and you silently install with an empty key.&lt;/p>
&lt;/blockquote>
&lt;h3 id="step-1--kind-cluster">Step 1 — kind cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --image kindest/node:v1.35.0 --wait 120s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-2--agent-substrate">Step 2 — Agent Substrate&lt;/h3>
&lt;p>JWT auth (ServiceAccount tokens) is the chart default — no feature gates.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --create-namespace --wait
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --wait --timeout 10m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n ate-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expect &lt;code>ate-api-server&lt;/code>, &lt;code>ate-controller&lt;/code>, &lt;code>atelet-*&lt;/code>, &lt;code>atenet-router&lt;/code>, &lt;code>valkey-cluster-0&lt;/code>..&lt;code>-5&lt;/code>, &lt;code>rustfs&lt;/code> Running (plus Completed init Jobs).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pod&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>ate-api-server&lt;/code>&lt;/td>
&lt;td>Control plane: lifecycle, scheduling, suspend/resume&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ate-controller&lt;/code>&lt;/td>
&lt;td>Reconciles &lt;code>WorkerPool&lt;/code> + &lt;code>ActorTemplate&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atelet&lt;/code>&lt;/td>
&lt;td>Node supervisor: images, sandbox, object storage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atenet-router&lt;/code>&lt;/td>
&lt;td>L7 route to the active worker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>valkey-cluster-*&lt;/code>&lt;/td>
&lt;td>Actor/worker state + locks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>rustfs&lt;/code>&lt;/td>
&lt;td>In-cluster S3 for snapshots&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="step-3--kagent-wired-to-substrate">Step 3 — kagent wired to substrate&lt;/h3>
&lt;p>&lt;strong>Order matters:&lt;/strong> install substrate first. The kagent controller crash-loops if ateapi is unreachable at startup.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent --create-namespace --wait
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --namespace kagent --timeout 10m --wait &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKey&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiEndpoint&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dns:///api.ate-system.svc:443&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiInsecure&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.atenetRouterURL&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://atenet-router.ate-system.svc:80&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiTokenFile&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/run/secrets/tokens/ate-api/token&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.namespace&lt;span class="o">=&lt;/span>kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.create&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.replicas&lt;span class="o">=&lt;/span>&lt;span class="m">2&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.ateomImage&lt;span class="o">=&lt;/span>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set grafana-mcp.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set observability-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Flag&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>controller.substrate.enabled=true&lt;/code>&lt;/td>
&lt;td>Turn on integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>controller.substrate.ateApiEndpoint&lt;/code>&lt;/td>
&lt;td>Substrate control plane&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>controller.substrate.atenetRouterURL&lt;/code>&lt;/td>
&lt;td>Request router&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>substrateWorkerPool.create=true&lt;/code> + &lt;code>.replicas=2&lt;/code>&lt;/td>
&lt;td>Two warm workers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>grafana / observability agents off&lt;/td>
&lt;td>Avoid broken MCP when Grafana isn&amp;rsquo;t installed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If Helm times out on cold start:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> deploy/kagent-controller -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Available --timeout&lt;span class="o">=&lt;/span>10m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get secret kagent-openai -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get workerpools.ate.dev -A &lt;span class="c1"># expect kagent/kagent-default&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward deploy/kagent-controller 8083:8083 &amp;gt;/tmp/pf-ctrl.log 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sleep &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8083/api/substrate/status&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># expect &amp;#34;enabled&amp;#34;: true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">kill&lt;/span> %1 2&amp;gt;/dev/null
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-4--deploy-a-sandboxagent">Step 4 — Deploy a SandboxAgent&lt;/h3>
&lt;p>Manifest (also in the demo repo as &lt;code>manifests/hello-substrate.yaml&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SandboxAgent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hello-substrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">A Kubernetes assistant running inside a substrate gVisor actor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Go runtime is required for gVisor checkpoint/restore on substrate.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are a helpful Kubernetes assistant running inside an Agent Substrate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> gVisor actor. Use the Kubernetes tools to answer questions about the
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cluster. When asked who you are, say &amp;#34;I am a Kubernetes agent running
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> inside a gVisor actor on Agent Substrate.&amp;#34; Keep answers concise.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_pod_logs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">substrate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workerPoolRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f manifests/hello-substrate.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> sandboxagent/hello-substrate -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Ready --timeout&lt;span class="o">=&lt;/span>5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get sandboxagent -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get actortemplates.ate.dev -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n kagent -l ate.dev/worker-pool&lt;span class="o">=&lt;/span>kagent-default -o wide
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The first &lt;strong>golden snapshot&lt;/strong> takes ~60–90s. kagent projects the &lt;code>SandboxAgent&lt;/code> into an owned &lt;code>ActorTemplate&lt;/code> (and secrets); you don&amp;rsquo;t hand-write the ate.dev CRDs for the happy path.&lt;/p>
&lt;h3 id="step-5--chat-and-watch-suspend--resume">Step 5 — Chat and watch suspend / resume&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward svc/kagent-ui 8080:8080
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># http://localhost:8080 → Agents → hello-substrate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;strong>Agents&lt;/strong> view lists every agent across namespaces — &lt;code>hello-substrate&lt;/code> (our declarative Kubernetes assistant on the default pool) sits alongside the built-in kagent library (k8s, Helm, Istio, Cilium, kgateway, …). Open it to start a conversation.&lt;/p>
&lt;img src="https://maniak.io/images/articles/2026-07-13-kagent-substrate/kagent-agents-cards.png" alt="kagent OSS UI, Agents view in card layout showing agents across all namespaces — kagent/hello-substrate ('A Kubernetes assistant running inside a substrate gVisor actor') alongside argo-rollouts-conversion, cilium-debug, cilium-manager, cilium-policy, helm, istio, k8s, and kgateway agents, each backed by OpenAI (gpt-4.1-mini)." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;p>Try:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>What are you, and where are you running? Answer in one sentence.&lt;/em>&lt;br>
&lt;em>List pods in ate-system.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>Between requests the session actor should sit &lt;strong>Suspended&lt;/strong> (UI &lt;strong>View → Substrate&lt;/strong>, or CLI below). Next message restores it sub-second.&lt;/p>
&lt;p>The &lt;strong>View → Substrate&lt;/strong> page is where the multiplexing thesis becomes visible: the &lt;code>kagent-default&lt;/code> &lt;strong>WorkerPool&lt;/strong> (2 replicas on &lt;code>ateom-gvisor:v0.0.8&lt;/code>), the &lt;code>hello-substrate&lt;/code> &lt;strong>ActorTemplate&lt;/strong> (&lt;code>READY&lt;/code>, sandbox class &lt;code>gvisor&lt;/code>, backed by its golden snapshot), the live &lt;strong>actor&lt;/strong> flipping to &lt;code>SUSPENDED&lt;/code> between turns, and both &lt;strong>workers&lt;/strong> sitting &lt;code>idle&lt;/code> — no pod tax while the session waits.&lt;/p>
&lt;img src="https://maniak.io/images/articles/2026-07-13-kagent-substrate/substrate-view.gif" alt="kagent OSS UI, View → Substrate page: the kagent-default WorkerPool with 2 replicas on ateom-gvisor:v0.0.8, the hello-substrate ActorTemplate in READY phase with sandbox class gvisor and a golden snapshot, a single actor in SUSPENDED status mapped to that template with no worker pod assigned, and two kagent-default worker pods both marked idle." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;h4 id="drive-ate-api-with-grpcurl">Drive ate-api with grpcurl&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n ate-system svc/api 18443:443 &amp;gt;/tmp/pf-api.log 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sleep &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl create token kagent-controller -n kagent --audience&lt;span class="o">=&lt;/span>api.ate-system.svc --duration&lt;span class="o">=&lt;/span>15m&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListWorkers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListActors
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ACTORS_JSON&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListActors&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ACTOR_ID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ACTORS_JSON&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq -r &lt;span class="s1">&amp;#39;.actors[0].actorId // empty&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ATESPACE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ACTORS_JSON&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq -r &lt;span class="s1">&amp;#39;.actors[0].atespace // empty&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ResumeActor expects actor_ref { atespace, name } — not a bare actor_id field.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ACTOR_ID&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ATESPACE&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;{\&amp;#34;actor_ref\&amp;#34;:{\&amp;#34;atespace\&amp;#34;:\&amp;#34;&lt;/span>&lt;span class="nv">$ATESPACE&lt;/span>&lt;span class="s2">\&amp;#34;,\&amp;#34;name\&amp;#34;:\&amp;#34;&lt;/span>&lt;span class="nv">$ACTOR_ID&lt;/span>&lt;span class="s2">\&amp;#34;}}&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ResumeActor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="scale-the-workerpool">Scale the WorkerPool&lt;/h3>
&lt;p>One worker can serve many &lt;strong>sequential&lt;/strong> declarative sessions (each releases the slot after snapshot). Scale when you need overlapping sessions or harnesses that pin a slot:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale workerpool kagent-default -n kagent --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="how-kagent-and-substrate-fit-together">How kagent and substrate fit together&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">UI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kagent&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Agent&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">SandboxAgent&lt;/span> &lt;span class="n">CRDs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tools&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">CreateActor&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">Resume&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">chat&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Agent&lt;/span> &lt;span class="n">Substrate&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">ateapi&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">atenet&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">atelet&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">workers&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">gVisor&lt;/span> &lt;span class="n">sandbox&lt;/span> &lt;span class="n">per&lt;/span> &lt;span class="n">assigned&lt;/span> &lt;span class="n">actor&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Your&lt;/span> &lt;span class="n">agent&lt;/span> &lt;span class="n">binary&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Go&lt;/span> &lt;span class="n">ADK&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>Without substrate:&lt;/strong> kagent runs agents as ordinary Kubernetes Deployments (always-on pods).&lt;/li>
&lt;li>&lt;strong>With substrate:&lt;/strong> &lt;code>SandboxAgent&lt;/code> + &lt;code>spec.substrate.workerPoolRef&lt;/code> → actor on the pool; suspend when idle.&lt;/li>
&lt;/ul>
&lt;p>Christian Posta&amp;rsquo;s write-up frames the product story: agents are long-lived but idle; you need isolation (gVisor/Firecracker) &lt;strong>and&lt;/strong> real lifecycle (suspend, snapshot, resume). Substrate + kagent is that path open-sourced under the &lt;a href="https://kagent.dev">kagent&lt;/a> umbrella. See also &lt;a href="https://www.solo.io/blog/agent-substrate-powers-kubernetes-agents-with-kagent">Solo&amp;rsquo;s blog&lt;/a> and the official &lt;a href="https://kagent.dev/docs/kagent/examples/agent-substrate">kagent Agent Substrate example&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="known-issues--troubleshooting">Known issues &amp;amp; troubleshooting&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Issue&lt;/th>
&lt;th>Detail&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Default pairing&lt;/strong>&lt;/td>
&lt;td>Pin &lt;strong>substrate 0.0.8&lt;/strong> + &lt;strong>kagent 0.10.0-beta6&lt;/strong> for working chat protos.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Empty OpenAI key&lt;/strong>&lt;/td>
&lt;td>Install “succeeds” without &lt;code>kagent-openai&lt;/code> → &lt;code>CreateContainerConfigError&lt;/code>. Export key, re-run &lt;code>./setup.sh&lt;/code> or Helm.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Controller crash-loop&lt;/strong>&lt;/td>
&lt;td>Substrate not ready when kagent starts — fix &lt;code>ate-system&lt;/code> first.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CRDs rejected&lt;/strong>&lt;/td>
&lt;td>Node image &amp;lt; 1.31 — recreate with &lt;code>kindest/node:v1.35.0&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TLS on idle kind&lt;/strong>&lt;/td>
&lt;td>In-cluster certs can expire after ~24h — run in one sitting.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Pre-1.0&lt;/strong>&lt;/td>
&lt;td>APIs will change; not production-ready.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Symptom&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Substrate CRDs rejected&lt;/td>
&lt;td>Recreate cluster with k8s 1.31+ node image&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-controller&lt;/code> crash-loops&lt;/td>
&lt;td>All &lt;code>ate-system&lt;/code> pods Running before kagent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chat fails / ConfigError&lt;/td>
&lt;td>&lt;code>kubectl get secret kagent-openai -n kagent&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Helm timeout on kagent&lt;/td>
&lt;td>&lt;code>kubectl wait deploy/kagent-controller … --timeout=10m&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ListActors&lt;/code> auth errors&lt;/td>
&lt;td>Re-mint token (15m)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SandboxAgent not Ready&lt;/td>
&lt;td>&lt;code>WORKER_POOL_REPLICAS&amp;gt;=2&lt;/code>; check workers + ActorTemplate status&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./teardown.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or: kind delete cluster --name kagent-substrate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="whats-next">What&amp;rsquo;s next&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">&lt;code>01-kagent-agent-substrate&lt;/code>&lt;/a>&lt;/td>
&lt;td>Runnable code for this guide: &lt;code>setup.sh&lt;/code>, &lt;code>teardown.sh&lt;/code>, manifests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a>&lt;/td>
&lt;td>Visual topology, resume flow, ateapi internals&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://kagent.dev/docs/kagent/examples/agent-harness">AgentHarness on kagent&lt;/a>&lt;/td>
&lt;td>Long-lived coding sandboxes on substrate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/td>
&lt;td>Govern LLM/MCP egress; keep keys off the actor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://kagent.dev/docs/kagent">kagent docs&lt;/a>&lt;/td>
&lt;td>Agents, tools, MCP, observability&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Suspend-and-resume is the feature that finally makes &lt;strong>per-session stateful agents&lt;/strong> affordable at scale on Kubernetes. Spin it up, chat with &lt;code>hello-substrate&lt;/code>, and watch a small worker pool serve far more sessions than it has pods.&lt;/p></description><content:encoded>&lt;h1 id="code-share-deploy-kagent-with-agent-substrate">Code Share: Deploy kagent with Agent Substrate&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>Agent sessions are bursty. A user asks a question, the agent thinks for a few seconds, then the session sits idle for minutes — or hours — waiting on the next turn. Plain Kubernetes handles this badly: an idle pod still books its CPU and memory, and a cold pod takes seconds to come back. Multiply that across thousands of conversations and you&amp;rsquo;re paying for a lot of nothing.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://github.com/agent-substrate/substrate">Agent Substrate&lt;/a>&lt;/strong> flips the model. It decouples the &lt;em>agent session&lt;/em> from the &lt;em>pod&lt;/em>: idle sessions are checkpointed — full RAM and filesystem, via &lt;a href="https://gvisor.dev/">gVisor&lt;/a> — to object storage, the pod returns to a warm pool, and the session resumes &lt;strong>sub-second&lt;/strong> on the next request, exactly where it left off. Think serverless scale-to-zero, but for &lt;em>stateful&lt;/em> agents.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://kagent.dev">kagent&lt;/a>&lt;/strong> is the Kubernetes-native agent control plane. Wire substrate in as its execution layer and a declarative &lt;code>SandboxAgent&lt;/code> becomes a gVisor &lt;strong>actor&lt;/strong> instead of a long-running Deployment.&lt;/p>
&lt;p>This guide covers three things:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>What a substrate is&lt;/strong> (concepts from &lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a>)&lt;/li>
&lt;li>&lt;strong>What you can actually accomplish with it&lt;/strong> (use cases)&lt;/li>
&lt;li>&lt;strong>How to set it up&lt;/strong> with kagent OSS on a throwaway &lt;a href="https://kind.sigs.k8s.io/">kind&lt;/a> cluster — one-shot script or manual Helm&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>Runnable code for this path lives in &lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">&lt;code>01-kagent-agent-substrate&lt;/code>&lt;/a> (or your local kagent-demos clone). Prefer a guided lab? Same flow in the &lt;a href="https://github.com/sebbycorp/Instruqt-demos/tree/main/01-kagent-agent-substrate-workshop">Instruqt workshop&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="what-is-agent-substrate">What is Agent Substrate?&lt;/h2>
&lt;p>From the official atlas at &lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a>:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Agent Substrate&lt;/strong> is a Kubernetes-native runtime for highly-multiplexed actor workloads — AI agents, sandboxed environments, stateful services. It decouples actor lifecycle from Pods, so a small pool of pre-warmed gVisor workers can host &lt;strong>30× more actors than there are pods&lt;/strong>, by suspending idle actors to object storage and restoring them on demand.&lt;/p>
&lt;/blockquote>
&lt;p>Kubernetes is excellent at long-running services. It is not excellent at:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pain&lt;/th>
&lt;th>Why K8s struggles&lt;/th>
&lt;th>What substrate does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Idle agents&lt;/td>
&lt;td>Pods still consume CPU/memory&lt;/td>
&lt;td>Suspend to object storage; reclaim the worker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Millions of sessions&lt;/td>
&lt;td>API server / etcd not built for that QPS&lt;/td>
&lt;td>Actors live in Valkey/Redis, not as one CR per session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sub-second wake&lt;/td>
&lt;td>kube-scheduler + image pull is seconds&lt;/td>
&lt;td>Pre-warmed workers; resume bypasses the scheduler&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Stateful scale-to-zero&lt;/td>
&lt;td>Volumes don&amp;rsquo;t attach/detach at agent speed&lt;/td>
&lt;td>Full RAM + filesystem checkpoint via gVisor&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Architecture bet in one sentence: &lt;strong>Kubernetes provisions infrastructure; substrate schedules actors.&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="core-concepts-glossary">Core concepts (glossary)&lt;/h2>
&lt;p>These terms show up everywhere in the UI, CRDs, and &lt;code>grpcurl&lt;/code> surface. Definitions match &lt;a href="https://learn.agentsubstrate.dev/concepts/actor/">learn.agentsubstrate.dev/concepts&lt;/a>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Term&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Actor&lt;/strong>&lt;/td>
&lt;td>One logical agent session. Has its own RAM, filesystem, and identity — but is &lt;strong>not pinned to a pod&lt;/strong>. Can suspend on worker A and resume on worker B.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Worker&lt;/strong>&lt;/td>
&lt;td>A pre-warmed pod that hosts &lt;strong>at most one&lt;/strong> actor at a time (&lt;code>IDLE&lt;/code> or assigned). Fungible hosting slot, not the agent itself.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>WorkerPool&lt;/strong>&lt;/td>
&lt;td>CRD for a Deployment of warm workers. You size concurrency by scaling the pool.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ActorTemplate&lt;/strong>&lt;/td>
&lt;td>Immutable “class” for actors (image, entrypoint, pool, snapshot location). Creating one builds a &lt;strong>golden snapshot&lt;/strong> (version 0).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden snapshot&lt;/strong>&lt;/td>
&lt;td>Fresh, just-booted image of the template. Brand-new actors restore from this instead of cold-booting.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Snapshot&lt;/strong>&lt;/td>
&lt;td>Checkpoint of RAM + sentry/filesystem state in S3/GCS (zstd). Resume uses demand paging so only touched pages load.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Suspend / Resume&lt;/strong>&lt;/td>
&lt;td>Checkpoint to object storage and free the worker / restore sub-second into an idle worker.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ateapi&lt;/strong>&lt;/td>
&lt;td>Control plane: actor lifecycle, worker assignment, suspend/resume workflows (gRPC).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>atenet&lt;/strong>&lt;/td>
&lt;td>L7 router + DNS. Per-request resolve of which worker hosts an actor; triggers resume if suspended.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>atelet&lt;/strong>&lt;/td>
&lt;td>Node DaemonSet: pulls images, downloads snapshots, talks to &lt;code>ateom-gvisor&lt;/code> over a Unix socket.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ateom-gvisor&lt;/strong>&lt;/td>
&lt;td>In-worker helper that shells out to &lt;code>runsc checkpoint&lt;/code> / &lt;code>runsc restore&lt;/code>.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="actor-lifecycle">Actor lifecycle&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">CreateActor → SUSPENDED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ResumeActor → RESUMING → RUNNING
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ SuspendActor → SUSPENDING → SUSPENDED
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── DeleteActor (from SUSPENDED)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Only &lt;strong>RUNNING&lt;/strong> holds a worker. Between chat turns a declarative session is typically &lt;strong>SUSPENDED&lt;/strong>.&lt;/p>
&lt;h3 id="request-path-why-resume-is-fast">Request path (why resume is fast)&lt;/h3>
&lt;p>On each HTTP hit to an actor:&lt;/p>
&lt;ol>
&lt;li>DNS resolves &lt;code>actorId.actors.resources.substrate.ate.dev&lt;/code> → &lt;strong>atenet router&lt;/strong> (not the worker IP).&lt;/li>
&lt;li>ExtProc extracts the actor ID and calls &lt;strong>ateapi &lt;code>ResumeActor&lt;/code>&lt;/strong>.&lt;/li>
&lt;li>ateapi picks an idle worker from Redis (no kube-scheduler), asks &lt;strong>atelet&lt;/strong> to restore the snapshot.&lt;/li>
&lt;li>&lt;strong>ateom-gvisor&lt;/strong> runs &lt;code>runsc restore -background&lt;/code> — sentry comes up immediately; pages fault in on demand.&lt;/li>
&lt;li>Router rewrites &lt;code>:authority&lt;/code> to the worker pod and forwards the request.&lt;/li>
&lt;/ol>
&lt;p>Warm path (already RUNNING): skip restore, just route. Cold path (SUSPENDED): restore + route. No idle pod tax either way.&lt;/p>
&lt;p>Deep dive: &lt;a href="https://learn.agentsubstrate.dev/flows/resume-actor/">Resume actor end-to-end&lt;/a> · &lt;a href="https://learn.agentsubstrate.dev/topology/">System topology&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="what-can-you-accomplish-use-cases">What can you accomplish? (use cases)&lt;/h2>
&lt;p>Substrate is framework-agnostic at the OCI/gVisor layer. With &lt;strong>kagent OSS&lt;/strong> on top, these are the practical outcomes.&lt;/p>
&lt;h3 id="1-dense-multi-session-chat-agents-this-guide">1. Dense multi-session chat agents (this guide)&lt;/h3>
&lt;p>Many concurrent &lt;strong>declarative&lt;/strong> conversations without one Deployment per session.&lt;/p>
&lt;ul>
&lt;li>Each UI/chat session becomes a short-lived &lt;strong>actor&lt;/strong>.&lt;/li>
&lt;li>After the turn, the actor snapshots back; the worker serves the next session.&lt;/li>
&lt;li>One small WorkerPool multiplexes far more sessions than it has pods (~30× oversubscription is the design point).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>You get:&lt;/strong> serverless economics for stateful chat, with memory and filesystem preserved across turns.&lt;/p>
&lt;h3 id="2-sandboxed-cluster-assistants-kubernetes-tools-in-a-box">2. Sandboxed cluster assistants (Kubernetes tools in a box)&lt;/h3>
&lt;p>Run a &lt;code>SandboxAgent&lt;/code> with MCP tools (&lt;code>k8s_get_resources&lt;/code>, logs, events) &lt;strong>inside gVisor&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>Isolation: hostile or buggy tool use stays in the sandbox.&lt;/li>
&lt;li>Density: idle assistants don&amp;rsquo;t pin CPUs.&lt;/li>
&lt;li>Identity: actor is a first-class substrate identity, not “whatever ServiceAccount the pod had forever.”&lt;/li>
&lt;/ul>
&lt;p>This demo&amp;rsquo;s &lt;code>hello-substrate&lt;/code> is exactly that pattern — a Kubernetes assistant actor on the default pool.&lt;/p>
&lt;h3 id="3-coding-harnesses-and-long-lived-sandboxes-agentharness">3. Coding harnesses and long-lived sandboxes (AgentHarness)&lt;/h3>
&lt;p>kagent can place &lt;strong>OpenClaw / Hermes-style&lt;/strong> harnesses on substrate (&lt;code>runtime: substrate&lt;/code>).&lt;/p>
&lt;ul>
&lt;li>Shared actor for a coding session or workspace.&lt;/li>
&lt;li>Pins a worker while active (scale the pool: roughly &lt;code>1 + active harnesses&lt;/code> for headroom).&lt;/li>
&lt;li>Strong isolation for shell, git, and untrusted code.&lt;/li>
&lt;/ul>
&lt;p>See the &lt;a href="https://kagent.dev/docs/kagent/examples/agent-harness">kagent AgentHarness docs&lt;/a> after this walkthrough.&lt;/p>
&lt;h3 id="4-agent-swarms--many-actor-demos">4. Agent swarms / many-actor demos&lt;/h3>
&lt;p>Substrate&amp;rsquo;s own demos (e.g. many stateful counter actors on few workers) show the &lt;strong>multiplexing&lt;/strong> thesis: hundreds of stateful actors on a handful of pods.&lt;/p>
&lt;p>&lt;strong>You get:&lt;/strong> a path toward “millions of idle agents, thousands of wakeups/sec” without etcd holding every session.&lt;/p>
&lt;h3 id="5-security-minded-agent-platforms">5. Security-minded agent platforms&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>gVisor&lt;/strong> (or micro-VM class) per actor.&lt;/li>
&lt;li>Egress can sit behind &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> so agents never hold provider keys (Solo&amp;rsquo;s enterprise pattern).&lt;/li>
&lt;li>Snapshot-to-storage means you can reclaim compute without losing session state.&lt;/li>
&lt;/ul>
&lt;p>North-star metrics from the architecture docs: ~100 ms activation p95, extreme scale of idle actors, high wakeup throughput. Treat those as direction, not a SLA for the kind lab.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-on-kind">Architecture on kind&lt;/h2>
&lt;p>What you will run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────────────────── kind cluster (kagent-substrate) ────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ namespace: kagent namespace: ate-system │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────────────┐ ┌──────────────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-controller │ controller.substrate.* │ ate-api-server (scheduling) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-ui │ ───────────────────────▶│ atenet-router (L7 routing) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ SandboxAgent │ │ │ atelet (node supervisor) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ hello-substrate │ │ │ valkey-cluster (state) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ WorkerPool │ │ │ rustfs (snapshots/S3) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-default │ │ └──────────────────────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent Substrate&lt;/strong> (&lt;code>ate-system&lt;/code>)&lt;/td>
&lt;td>Multiplex actors onto warm workers; suspend/resume via gVisor + object storage.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong> (&lt;code>kagent&lt;/code>)&lt;/td>
&lt;td>Agent CRDs, UI, model provider (OpenAI); substrate as execution layer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong>&lt;/td>
&lt;td>Declarative agent → substrate &lt;strong>actor&lt;/strong> (not a long-running Deployment).&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="pinned-versions-this-guide">Pinned versions (this guide)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Version&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Agent Substrate&lt;/td>
&lt;td>&lt;code>0.0.8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kagent (OSS)&lt;/td>
&lt;td>&lt;code>0.10.0-beta6&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gVisor actor image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.8&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kind&lt;/td>
&lt;td>&lt;code>v0.31.0&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Node image&lt;/td>
&lt;td>&lt;code>kindest/node:v1.35.0&lt;/code> (any &lt;strong>1.31+&lt;/strong> works)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Pairing matters.&lt;/strong> Substrate &lt;strong>0.0.8&lt;/strong> matches kagent &lt;strong>0.10.0-beta6&lt;/strong> ateapi/&lt;code>CreateActor&lt;/code> protos so UI chat works. Substrate &lt;strong>0.0.9&lt;/strong> broke that wire format — only bump together with a matching kagent.&lt;br>
&lt;strong>Node image matters.&lt;/strong> Substrate CRDs use CEL &lt;code>format.dns1123Label&lt;/code> / &lt;code>dns1123Subdomain&lt;/code> (Kubernetes &lt;strong>1.31+&lt;/strong>). Old kind defaults reject the CRDs.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Linux&lt;/strong> host or VM (~&lt;strong>8 vCPU / 16 GB RAM&lt;/strong>). Valkey (6) + control plane is heavy for a laptop-sized VM.&lt;/li>
&lt;li>Docker running&lt;/li>
&lt;li>OpenAI API key (real LLM calls)&lt;/li>
&lt;li>Optional: &lt;code>grpcurl&lt;/code> + &lt;code>jq&lt;/code> for the control-plane section&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>macOS / Windows:&lt;/strong> prefer a cloud Linux VM (&lt;code>n1-standard-8&lt;/code>, &lt;code>m5.2xlarge&lt;/code>, …). Docker Desktop often fails the gVisor checkpoint path.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="quick-start-one-shot-script">Quick start (one-shot script)&lt;/h2>
&lt;p>From the demo directory:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># optional: cp .env.example .env &amp;amp;&amp;amp; edit .env&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x setup.sh teardown.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>setup.sh&lt;/code> installs tools if missing, creates kind, installs substrate + kagent (wired), and applies &lt;code>manifests/hello-substrate.yaml&lt;/code>.&lt;/p>
&lt;p>Open the UI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward svc/kagent-ui 8080:8080
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># → http://localhost:8080 → Agents → hello-substrate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tear down:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./teardown.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="script-knobs">Script knobs&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Env var&lt;/th>
&lt;th>Default&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>OPENAI_API_KEY&lt;/code>&lt;/td>
&lt;td>&lt;em>(required)&lt;/em>&lt;/td>
&lt;td>Model provider key&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>KIND_CLUSTER&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-substrate&lt;/code>&lt;/td>
&lt;td>kind cluster name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SUBSTRATE_VERSION&lt;/code>&lt;/td>
&lt;td>&lt;code>0.0.8&lt;/code>&lt;/td>
&lt;td>Substrate chart&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>KAGENT_VERSION&lt;/code>&lt;/td>
&lt;td>&lt;code>0.10.0-beta6&lt;/code>&lt;/td>
&lt;td>kagent chart&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>WORKER_POOL_REPLICAS&lt;/code>&lt;/td>
&lt;td>&lt;code>2&lt;/code>&lt;/td>
&lt;td>Warm workers (2 helps golden-snapshot blue-green)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SKIP_TOOLS=1&lt;/code>&lt;/td>
&lt;td>off&lt;/td>
&lt;td>Don&amp;rsquo;t auto-install kubectl/helm/kind/…&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SKIP_CLUSTER=1&lt;/code>&lt;/td>
&lt;td>off&lt;/td>
&lt;td>Reuse existing cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>SKIP_AGENT=1&lt;/code>&lt;/td>
&lt;td>off&lt;/td>
&lt;td>Skip applying the sample agent&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="manual-walkthrough-same-path-as-the-script">Manual walkthrough (same path as the script)&lt;/h2>
&lt;h3 id="env">Env&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="o">=&lt;/span>kagent-substrate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.0.8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.10.0-beta6
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>⚠️ Do &lt;strong>not&lt;/strong> write &lt;code>OPENAI_API_KEY=... helm ... --set ...=&amp;quot;${OPENAI_API_KEY}&amp;quot;&lt;/code> on one line — the variable expands &lt;em>before&lt;/em> the assignment and you silently install with an empty key.&lt;/p>
&lt;/blockquote>
&lt;h3 id="step-1--kind-cluster">Step 1 — kind cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --image kindest/node:v1.35.0 --wait 120s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-2--agent-substrate">Step 2 — Agent Substrate&lt;/h3>
&lt;p>JWT auth (ServiceAccount tokens) is the chart default — no feature gates.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --create-namespace --wait
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --wait --timeout 10m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n ate-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expect &lt;code>ate-api-server&lt;/code>, &lt;code>ate-controller&lt;/code>, &lt;code>atelet-*&lt;/code>, &lt;code>atenet-router&lt;/code>, &lt;code>valkey-cluster-0&lt;/code>..&lt;code>-5&lt;/code>, &lt;code>rustfs&lt;/code> Running (plus Completed init Jobs).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pod&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>ate-api-server&lt;/code>&lt;/td>
&lt;td>Control plane: lifecycle, scheduling, suspend/resume&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ate-controller&lt;/code>&lt;/td>
&lt;td>Reconciles &lt;code>WorkerPool&lt;/code> + &lt;code>ActorTemplate&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atelet&lt;/code>&lt;/td>
&lt;td>Node supervisor: images, sandbox, object storage&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atenet-router&lt;/code>&lt;/td>
&lt;td>L7 route to the active worker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>valkey-cluster-*&lt;/code>&lt;/td>
&lt;td>Actor/worker state + locks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>rustfs&lt;/code>&lt;/td>
&lt;td>In-cluster S3 for snapshots&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="step-3--kagent-wired-to-substrate">Step 3 — kagent wired to substrate&lt;/h3>
&lt;p>&lt;strong>Order matters:&lt;/strong> install substrate first. The kagent controller crash-loops if ateapi is unreachable at startup.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent --create-namespace --wait
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --namespace kagent --timeout 10m --wait &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKey&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiEndpoint&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dns:///api.ate-system.svc:443&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiInsecure&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.atenetRouterURL&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://atenet-router.ate-system.svc:80&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiTokenFile&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/run/secrets/tokens/ate-api/token&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.namespace&lt;span class="o">=&lt;/span>kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.create&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.replicas&lt;span class="o">=&lt;/span>&lt;span class="m">2&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.ateomImage&lt;span class="o">=&lt;/span>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set grafana-mcp.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set observability-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Flag&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>controller.substrate.enabled=true&lt;/code>&lt;/td>
&lt;td>Turn on integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>controller.substrate.ateApiEndpoint&lt;/code>&lt;/td>
&lt;td>Substrate control plane&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>controller.substrate.atenetRouterURL&lt;/code>&lt;/td>
&lt;td>Request router&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>substrateWorkerPool.create=true&lt;/code> + &lt;code>.replicas=2&lt;/code>&lt;/td>
&lt;td>Two warm workers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>grafana / observability agents off&lt;/td>
&lt;td>Avoid broken MCP when Grafana isn&amp;rsquo;t installed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If Helm times out on cold start:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> deploy/kagent-controller -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Available --timeout&lt;span class="o">=&lt;/span>10m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get secret kagent-openai -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get workerpools.ate.dev -A &lt;span class="c1"># expect kagent/kagent-default&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward deploy/kagent-controller 8083:8083 &amp;gt;/tmp/pf-ctrl.log 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sleep &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8083/api/substrate/status&lt;span class="p">;&lt;/span> &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># expect &amp;#34;enabled&amp;#34;: true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">kill&lt;/span> %1 2&amp;gt;/dev/null
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-4--deploy-a-sandboxagent">Step 4 — Deploy a SandboxAgent&lt;/h3>
&lt;p>Manifest (also in the demo repo as &lt;code>manifests/hello-substrate.yaml&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SandboxAgent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hello-substrate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">A Kubernetes assistant running inside a substrate gVisor actor&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Go runtime is required for gVisor checkpoint/restore on substrate.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are a helpful Kubernetes assistant running inside an Agent Substrate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> gVisor actor. Use the Kubernetes tools to answer questions about the
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> cluster. When asked who you are, say &amp;#34;I am a Kubernetes agent running
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> inside a gVisor actor on Agent Substrate.&amp;#34; Keep answers concise.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_pod_logs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">substrate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">workerPoolRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f manifests/hello-substrate.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> sandboxagent/hello-substrate -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Ready --timeout&lt;span class="o">=&lt;/span>5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get sandboxagent -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get actortemplates.ate.dev -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n kagent -l ate.dev/worker-pool&lt;span class="o">=&lt;/span>kagent-default -o wide
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The first &lt;strong>golden snapshot&lt;/strong> takes ~60–90s. kagent projects the &lt;code>SandboxAgent&lt;/code> into an owned &lt;code>ActorTemplate&lt;/code> (and secrets); you don&amp;rsquo;t hand-write the ate.dev CRDs for the happy path.&lt;/p>
&lt;h3 id="step-5--chat-and-watch-suspend--resume">Step 5 — Chat and watch suspend / resume&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward svc/kagent-ui 8080:8080
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># http://localhost:8080 → Agents → hello-substrate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;strong>Agents&lt;/strong> view lists every agent across namespaces — &lt;code>hello-substrate&lt;/code> (our declarative Kubernetes assistant on the default pool) sits alongside the built-in kagent library (k8s, Helm, Istio, Cilium, kgateway, …). Open it to start a conversation.&lt;/p>
&lt;img src="https://maniak.io/images/articles/2026-07-13-kagent-substrate/kagent-agents-cards.png" alt="kagent OSS UI, Agents view in card layout showing agents across all namespaces — kagent/hello-substrate ('A Kubernetes assistant running inside a substrate gVisor actor') alongside argo-rollouts-conversion, cilium-debug, cilium-manager, cilium-policy, helm, istio, k8s, and kgateway agents, each backed by OpenAI (gpt-4.1-mini)." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;p>Try:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>What are you, and where are you running? Answer in one sentence.&lt;/em>&lt;br>
&lt;em>List pods in ate-system.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>Between requests the session actor should sit &lt;strong>Suspended&lt;/strong> (UI &lt;strong>View → Substrate&lt;/strong>, or CLI below). Next message restores it sub-second.&lt;/p>
&lt;p>The &lt;strong>View → Substrate&lt;/strong> page is where the multiplexing thesis becomes visible: the &lt;code>kagent-default&lt;/code> &lt;strong>WorkerPool&lt;/strong> (2 replicas on &lt;code>ateom-gvisor:v0.0.8&lt;/code>), the &lt;code>hello-substrate&lt;/code> &lt;strong>ActorTemplate&lt;/strong> (&lt;code>READY&lt;/code>, sandbox class &lt;code>gvisor&lt;/code>, backed by its golden snapshot), the live &lt;strong>actor&lt;/strong> flipping to &lt;code>SUSPENDED&lt;/code> between turns, and both &lt;strong>workers&lt;/strong> sitting &lt;code>idle&lt;/code> — no pod tax while the session waits.&lt;/p>
&lt;img src="https://maniak.io/images/articles/2026-07-13-kagent-substrate/substrate-view.gif" alt="kagent OSS UI, View → Substrate page: the kagent-default WorkerPool with 2 replicas on ateom-gvisor:v0.0.8, the hello-substrate ActorTemplate in READY phase with sandbox class gvisor and a golden snapshot, a single actor in SUSPENDED status mapped to that template with no worker pod assigned, and two kagent-default worker pods both marked idle." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;h4 id="drive-ate-api-with-grpcurl">Drive ate-api with grpcurl&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n ate-system svc/api 18443:443 &amp;gt;/tmp/pf-api.log 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sleep &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl create token kagent-controller -n kagent --audience&lt;span class="o">=&lt;/span>api.ate-system.svc --duration&lt;span class="o">=&lt;/span>15m&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListWorkers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListActors
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ACTORS_JSON&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListActors&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ACTOR_ID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ACTORS_JSON&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq -r &lt;span class="s1">&amp;#39;.actors[0].actorId // empty&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ATESPACE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$ACTORS_JSON&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq -r &lt;span class="s1">&amp;#39;.actors[0].atespace // empty&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ResumeActor expects actor_ref { atespace, name } — not a bare actor_id field.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ACTOR_ID&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">ATESPACE&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;{\&amp;#34;actor_ref\&amp;#34;:{\&amp;#34;atespace\&amp;#34;:\&amp;#34;&lt;/span>&lt;span class="nv">$ATESPACE&lt;/span>&lt;span class="s2">\&amp;#34;,\&amp;#34;name\&amp;#34;:\&amp;#34;&lt;/span>&lt;span class="nv">$ACTOR_ID&lt;/span>&lt;span class="s2">\&amp;#34;}}&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ResumeActor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="scale-the-workerpool">Scale the WorkerPool&lt;/h3>
&lt;p>One worker can serve many &lt;strong>sequential&lt;/strong> declarative sessions (each releases the slot after snapshot). Scale when you need overlapping sessions or harnesses that pin a slot:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale workerpool kagent-default -n kagent --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="how-kagent-and-substrate-fit-together">How kagent and substrate fit together&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">UI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">kagent&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Agent&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">SandboxAgent&lt;/span> &lt;span class="n">CRDs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tools&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">CreateActor&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">Resume&lt;/span> &lt;span class="n">on&lt;/span> &lt;span class="n">chat&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Agent&lt;/span> &lt;span class="n">Substrate&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">ateapi&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">atenet&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">atelet&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">workers&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">gVisor&lt;/span> &lt;span class="n">sandbox&lt;/span> &lt;span class="n">per&lt;/span> &lt;span class="n">assigned&lt;/span> &lt;span class="n">actor&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Your&lt;/span> &lt;span class="n">agent&lt;/span> &lt;span class="n">binary&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Go&lt;/span> &lt;span class="n">ADK&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>Without substrate:&lt;/strong> kagent runs agents as ordinary Kubernetes Deployments (always-on pods).&lt;/li>
&lt;li>&lt;strong>With substrate:&lt;/strong> &lt;code>SandboxAgent&lt;/code> + &lt;code>spec.substrate.workerPoolRef&lt;/code> → actor on the pool; suspend when idle.&lt;/li>
&lt;/ul>
&lt;p>Christian Posta&amp;rsquo;s write-up frames the product story: agents are long-lived but idle; you need isolation (gVisor/Firecracker) &lt;strong>and&lt;/strong> real lifecycle (suspend, snapshot, resume). Substrate + kagent is that path open-sourced under the &lt;a href="https://kagent.dev">kagent&lt;/a> umbrella. See also &lt;a href="https://www.solo.io/blog/agent-substrate-powers-kubernetes-agents-with-kagent">Solo&amp;rsquo;s blog&lt;/a> and the official &lt;a href="https://kagent.dev/docs/kagent/examples/agent-substrate">kagent Agent Substrate example&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="known-issues--troubleshooting">Known issues &amp;amp; troubleshooting&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Issue&lt;/th>
&lt;th>Detail&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Default pairing&lt;/strong>&lt;/td>
&lt;td>Pin &lt;strong>substrate 0.0.8&lt;/strong> + &lt;strong>kagent 0.10.0-beta6&lt;/strong> for working chat protos.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Empty OpenAI key&lt;/strong>&lt;/td>
&lt;td>Install “succeeds” without &lt;code>kagent-openai&lt;/code> → &lt;code>CreateContainerConfigError&lt;/code>. Export key, re-run &lt;code>./setup.sh&lt;/code> or Helm.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Controller crash-loop&lt;/strong>&lt;/td>
&lt;td>Substrate not ready when kagent starts — fix &lt;code>ate-system&lt;/code> first.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>CRDs rejected&lt;/strong>&lt;/td>
&lt;td>Node image &amp;lt; 1.31 — recreate with &lt;code>kindest/node:v1.35.0&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TLS on idle kind&lt;/strong>&lt;/td>
&lt;td>In-cluster certs can expire after ~24h — run in one sitting.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Pre-1.0&lt;/strong>&lt;/td>
&lt;td>APIs will change; not production-ready.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Symptom&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Substrate CRDs rejected&lt;/td>
&lt;td>Recreate cluster with k8s 1.31+ node image&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-controller&lt;/code> crash-loops&lt;/td>
&lt;td>All &lt;code>ate-system&lt;/code> pods Running before kagent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chat fails / ConfigError&lt;/td>
&lt;td>&lt;code>kubectl get secret kagent-openai -n kagent&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Helm timeout on kagent&lt;/td>
&lt;td>&lt;code>kubectl wait deploy/kagent-controller … --timeout=10m&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ListActors&lt;/code> auth errors&lt;/td>
&lt;td>Re-mint token (15m)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SandboxAgent not Ready&lt;/td>
&lt;td>&lt;code>WORKER_POOL_REPLICAS&amp;gt;=2&lt;/code>; check workers + ActorTemplate status&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./teardown.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or: kind delete cluster --name kagent-substrate&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="whats-next">What&amp;rsquo;s next&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">&lt;code>01-kagent-agent-substrate&lt;/code>&lt;/a>&lt;/td>
&lt;td>Runnable code for this guide: &lt;code>setup.sh&lt;/code>, &lt;code>teardown.sh&lt;/code>, manifests&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://learn.agentsubstrate.dev/">learn.agentsubstrate.dev&lt;/a>&lt;/td>
&lt;td>Visual topology, resume flow, ateapi internals&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://kagent.dev/docs/kagent/examples/agent-harness">AgentHarness on kagent&lt;/a>&lt;/td>
&lt;td>Long-lived coding sandboxes on substrate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/td>
&lt;td>Govern LLM/MCP egress; keep keys off the actor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://kagent.dev/docs/kagent">kagent docs&lt;/a>&lt;/td>
&lt;td>Agents, tools, MCP, observability&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Suspend-and-resume is the feature that finally makes &lt;strong>per-session stateful agents&lt;/strong> affordable at scale on Kubernetes. Spin it up, chat with &lt;code>hello-substrate&lt;/code>, and watch a small worker pool serve far more sessions than it has pods.&lt;/p></content:encoded></item><item><title>The 8 Principles of an AI Gateway (and Why They Matter) — with agentgateway from Solo</title><link>https://maniak.io/articles/2026-07-12-eight-principles-of-an-ai-gateway-agentgateway/</link><pubDate>Sun, 12 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-12-eight-principles-of-an-ai-gateway-agentgateway/</guid><description>&lt;p>You wouldn&amp;rsquo;t put a hundred microservices into production with no gateway,
no auth, no rate limits, and no logs. Yet that is exactly how most teams
ship AI agents today: an API key baked into a container, a direct line to
an LLM, and a growing pile of MCP tool servers that nobody is governing.&lt;/p>
&lt;p>Agents don&amp;rsquo;t fail the way microservices fail. A microservice returns a 500
and you page someone. An agent quietly sends your customer table to a
third‑party model, loops a thousand times on a bad prompt and burns your
monthly budget before lunch, or discovers a tool you never meant to expose
and happily calls it. The failure modes are new, so the control plane has
to be new too.&lt;/p>
&lt;p>That is what an &lt;strong>AI gateway&lt;/strong> is for. Not &amp;ldquo;an API gateway with an LLM
route&amp;rdquo; — a purpose‑built data plane that understands LLM traffic, MCP
sessions, and agent‑to‑agent calls, and applies policy to all three.&lt;/p>
&lt;p>This post walks through the &lt;strong>eight principles&lt;/strong> an AI gateway has to get
right, why each one matters in production, and how
&lt;a href="https://github.com/agentgateway/agentgateway">&lt;strong>agentgateway&lt;/strong>&lt;/a> — the
open‑source (CNCF) project, packaged and hardened by
&lt;a href="https://www.solo.io/">&lt;strong>Solo.io&lt;/strong>&lt;/a> as Enterprise agentgateway — implements
them. Every principle gets a &lt;em>why&lt;/em> and a &lt;em>how&lt;/em>, with config you can read.&lt;/p>
&lt;p>If you want the &amp;ldquo;why does this exist at all&amp;rdquo; version first, start with
&lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>
and &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="the-eight-principles-at-a-glance">The eight principles at a glance&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Principle&lt;/th>
&lt;th>The problem it solves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>&lt;strong>Unified LLM access&lt;/strong> (one API, any provider)&lt;/td>
&lt;td>Every provider has a different SDK, auth, and payload. Apps get locked to one vendor.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>&lt;strong>MCP tool federation&lt;/strong> (stdio/HTTP/SSE/OpenAPI)&lt;/td>
&lt;td>Each tool server is a separate connection, auth surface, and thing to secure.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;strong>Secure A2A agent discovery &amp;amp; collaboration&lt;/strong>&lt;/td>
&lt;td>Agents calling agents with no identity, no scoping, no audit.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>&lt;strong>Built‑in guardrails&lt;/strong> (regex / moderation / webhooks)&lt;/td>
&lt;td>PII, secrets, and prompt injection flow straight through to the model.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>&lt;strong>Multi‑auth security&lt;/strong> (JWT / OAuth / RBAC / CEL)&lt;/td>
&lt;td>One coarse API key can do everything. No per‑user, per‑tool authorization.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>&lt;strong>Rate limiting, budgets &amp;amp; failover routing&lt;/strong>&lt;/td>
&lt;td>One runaway loop drains the budget; one provider outage kills every agent.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>&lt;strong>Full observability&lt;/strong> (OpenTelemetry / TLS)&lt;/td>
&lt;td>No traces, no token accounting, no idea what a prompt cost or where it went.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>&lt;strong>YAML policy‑driven config&lt;/strong>&lt;/td>
&lt;td>Governance living in app code and dashboards can&amp;rsquo;t be reviewed, versioned, or rolled back.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>None of these is optional at scale. Skip one and it becomes the incident.
Let&amp;rsquo;s take them one at a time.&lt;/p>
&lt;hr>
&lt;h2 id="1-unified-llm-access--one-api-any-provider">1. Unified LLM access — one API, any provider&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The moment you have more than one model provider — and
you will, the day one provider has an outage or another ships something
better — you feel the tax. Anthropic, OpenAI, Gemini, Bedrock, Vertex, and
your self‑hosted vLLM each have their own SDK, auth headers, request shape,
streaming semantics, and error codes. If each application wires directly to
each provider, you&amp;rsquo;ve hard‑coded vendor lock‑in into every service, and
swapping a model means a code change and a redeploy.&lt;/p>
&lt;p>An AI gateway makes this a routing concern. Apps speak &lt;strong>one&lt;/strong> stable API to
the gateway — the widely‑adopted OpenAI‑style schema is the common wire
format, but the point isn&amp;rsquo;t OpenAI, it&amp;rsquo;s &lt;em>one&lt;/em> front door — and the gateway
translates to whatever provider sits behind the route: Anthropic today,
Gemini tomorrow, a local model for the cheap path. Changing or mixing models
becomes a config change, not a code change, and no application is coupled to
any single vendor.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> A backend declares the provider; a route maps
a path prefix to that backend and normalizes the API surface. Here&amp;rsquo;s a
single standalone config front‑ending both Anthropic and OpenAI behind one
port:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4001&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Your application points &lt;code>OPENAI_BASE_URL&lt;/code> (or &lt;code>ANTHROPIC_BASE_URL&lt;/code>) at the
gateway and never holds a provider key again. On Kubernetes the same idea
is expressed as an &lt;code>AgentgatewayBackend&lt;/code> with an &lt;code>ai.provider&lt;/code> block and an
&lt;code>HTTPRoute&lt;/code> — see
&lt;a href="https://maniak.io/articles/2026-02-11-your-first-ai-route-connecting-to-openai-opensource/">Your first AI route&lt;/a>.
The payoff shows up later: because everything is normalized here, principles
6 (failover), 7 (token accounting), and 8 (policy) get to work on a single,
consistent stream instead of N provider dialects.&lt;/p>
&lt;hr>
&lt;h2 id="2-mcp-tool-federation-stdio--http--sse--openapi">2. MCP tool federation (stdio / HTTP / SSE / OpenAPI)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The &lt;a href="https://modelcontextprotocol.io/">Model Context Protocol&lt;/a>
is how agents get hands — GitHub, Slack, a database, an internal API. But
MCP servers multiply fast, and each one is a separate connection, a
separate auth story, and a separate thing to monitor. Worse, MCP is not
plain request/response: it&amp;rsquo;s &lt;strong>stateful JSON‑RPC over long‑lived sessions&lt;/strong>,
with server‑initiated SSE events that must route back to the &lt;em>correct&lt;/em>
client session. A path‑based reverse proxy can&amp;rsquo;t do that correctly.&lt;/p>
&lt;p>Federation means the gateway presents &lt;strong>one&lt;/strong> MCP endpoint to the agent and
multiplexes it across many backend tool servers — regardless of whether a
given server speaks stdio, streamable HTTP, SSE, or is really an OpenAPI
service wrapped as MCP. The agent sees one tool catalog; the gateway owns
the fan‑out, the session affinity, and the per‑target security.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> A single MCP listener with multiple targets,
each using whatever transport the server actually speaks:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">everything&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stdio&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cmd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;exec&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-i&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;mcp-everything&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;node&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;dist/index.js&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp.tools.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">internal-api&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openapi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/config/openapi/orders.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">orders.internal.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The agent connects once; agentgateway keeps the JSON‑RPC session coherent,
routes SSE events back to the right client, and — critically — lets you
decide &lt;em>which&lt;/em> tools each caller may even see. That last part is where
federation meets authorization (principle 5). For the deep version of
multiplexing and how tool exposure affects token cost, see
&lt;a href="https://maniak.io/articles/2026-02-20-mcp-multiplexing-tool-access-agentgateway/">MCP multiplexing&lt;/a>
and &lt;a href="https://maniak.io/articles/2026-06-20-github-mcp-token-economics-agentgateway-tool-modes/">GitHub MCP token economics&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="3-secure-a2a-agent-discovery--collaboration">3. Secure A2A agent discovery &amp;amp; collaboration&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The next step past &amp;ldquo;agent calls tools&amp;rdquo; is &amp;ldquo;agent calls
agent.&amp;rdquo; A planner delegates to a researcher; the researcher calls a
summarizer. &lt;a href="https://a2a-protocol.org/">A2A&lt;/a> standardizes that handoff. But
without a gateway in the middle, agent‑to‑agent traffic is the wild west:
no shared identity, no way to scope what one agent may ask another to do,
and no audit trail when a delegated call does something expensive or
sensitive. Multi‑agent systems fail &lt;em>between&lt;/em> the agents, and that seam is
exactly where nobody is looking.&lt;/p>
&lt;p>An AI gateway treats A2A as first‑class traffic: agents are discoverable
through the gateway, every cross‑agent call carries identity, and the same
auth, rate‑limit, and observability policies that guard LLM and MCP traffic
also guard the handoffs. The blast radius of a misbehaving agent stays
contained because there&amp;rsquo;s a kill switch and a policy boundary at the seam.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> A2A is routed and secured like any other
backend — an A2A route carries JWT identity forward, applies RBAC on what
the calling agent is allowed to invoke, and emits the same traces as
everything else. The multi‑agent kill‑switch pattern (revoke one agent&amp;rsquo;s
access at the gateway and it&amp;rsquo;s instantly cut off from peers and tools) is
covered in
&lt;a href="https://maniak.io/articles/2026-02-21-multi-agent-architecture-agentgateway-kill-switch/">Multi‑agent architecture + kill switch&lt;/a>,
and a full A2A/MCP stack running on kagent is in
&lt;a href="https://maniak.io/articles/2026-07-11-agentx-spacexai-x-mcp-agentgateway-kagent/">AgentX&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="4-builtin-guardrails-regex--moderation--webhooks">4. Built‑in guardrails (regex / moderation / webhooks)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> This is the principle that keeps security teams up at
night. Between the user and the model — in both directions — you need a
decision point that can &lt;em>see&lt;/em> the content and act on it: redact a credit
card, block a prompt‑injection attempt, drop a response that leaks an
internal hostname, or send the payload to an external moderation service and
honor its verdict. Bolt this into every app and you get inconsistent
coverage and a dozen places to audit. It belongs in the gateway, inline on
the request and response path.&lt;/p>
&lt;p>Guardrails come in layers, and a good gateway supports all three:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Regex / pattern&lt;/strong> rules for the cheap, deterministic cases (SSNs,
API‑key shapes, email addresses).&lt;/li>
&lt;li>&lt;strong>Moderation&lt;/strong> via a model or classification service for the fuzzy cases
(toxicity, jailbreaks, category policy).&lt;/li>
&lt;li>&lt;strong>Webhook&lt;/strong> callouts to an external decision engine — e.g. &lt;strong>F5 AI
Guardrails&lt;/strong> — for enterprise‑grade scanning, redaction, and audit,
either inline (block before it reaches the model) or out‑of‑band.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Guardrail policies attach to a route and run
on the prompt and/or the completion:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;\b\d{3}-\d{2}-\d{4}\b&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># US SSN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails.security.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;(?i)internal[- ]only&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mask&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The point is that a blocked request returns a clean, inspectable decision —
security sees &lt;em>why&lt;/em> it was blocked, the app sees a status code, and nothing
leaked. The full inline + out‑of‑band F5 pattern is in
&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/">agentgateway + F5 AI Guardrails architectures&lt;/a>
and the hands‑on UI walkthrough is
&lt;a href="https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/">here&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="5-multiauth-security-jwt--oauth--rbac--cel">5. Multi‑auth security (JWT / OAuth / RBAC / CEL)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> A single API key is a skeleton key: whoever holds it can
do everything the key can do. That&amp;rsquo;s fine for a demo and catastrophic in
production. Real systems need &lt;em>who&lt;/em> (authentication), &lt;em>what are they allowed
to do&lt;/em> (authorization), and the ability to express that at the granularity
of &amp;ldquo;this team&amp;rsquo;s agents may call these three MCP tools and this model, but
not that one.&amp;rdquo; Coarse keys can&amp;rsquo;t express that. You need identity flowing
through the gateway and policy evaluated per request.&lt;/p>
&lt;p>An AI gateway should support:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>JWT&lt;/strong> validation — verify tokens, check issuer/audience, extract claims.&lt;/li>
&lt;li>&lt;strong>OAuth&lt;/strong> — front the gateway with an IdP (Okta, Keycloak, Entra) so
agents authenticate as real principals.&lt;/li>
&lt;li>&lt;strong>RBAC&lt;/strong> — map identities/claims to roles, and roles to what they may
reach.&lt;/li>
&lt;li>&lt;strong>CEL&lt;/strong> (&lt;a href="https://cel.dev/">Common Expression Language&lt;/a>) — fine‑grained,
attribute‑based rules like &amp;ldquo;allow only if &lt;code>jwt.groups&lt;/code> contains
&lt;code>platform&lt;/code> &lt;strong>and&lt;/strong> the requested tool isn&amp;rsquo;t in the destructive set.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> JWT auth is a policy on the listener or route;
RBAC and CEL express which authenticated principals may reach which backends
or MCP tools:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwtAuth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://login.example.com/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://login.example.com/.well-known/jwks.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># only the platform group may use write-capable tools&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;jwt.groups.exists(g, g == &amp;#34;platform&amp;#34;) || !request.mcp.tool.startsWith(&amp;#34;create_&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Identity established at the gateway then travels &lt;em>with&lt;/em> the request into
MCP and A2A calls (principles 2 and 3), so authorization is consistent end
to end instead of re‑invented per hop. For a full OAuth chain into a cloud
runtime, see
&lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">AWS AgentCore with agentgateway and Okta OAuth&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="6-rate-limiting-budgets--failover-routing">6. Rate limiting, budgets &amp;amp; failover routing&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> LLM traffic has a property normal traffic doesn&amp;rsquo;t:
&lt;strong>every request costs money&lt;/strong>, and a bad one can cost a lot. An agent stuck
in a reasoning loop doesn&amp;rsquo;t crash — it spends. So the gateway needs two
knobs traditional gateways never had:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Budgets&lt;/strong> — a hard ceiling in &lt;em>dollars or tokens&lt;/em> per user, per team,
per agent. When it&amp;rsquo;s hit, the gateway returns &lt;code>429&lt;/code>, not a surprise
invoice. This is FinOps as a policy, not a monthly spreadsheet.&lt;/li>
&lt;li>&lt;strong>Failover routing&lt;/strong> — provider A is the primary; on error, timeout, or
overload, the gateway transparently fails over to provider B (or a
cheaper/local model) without the app knowing. One outage stops being one
outage for every agent.&lt;/li>
&lt;/ul>
&lt;p>Plus classic &lt;strong>rate limiting&lt;/strong> (requests per second per principal) to
protect both your budget and the upstream provider&amp;rsquo;s limits.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Budgets attach a cost model and a ceiling; the
gateway meters real token usage against it. Failover is expressed as an
ordered/weighted backend list with health‑aware routing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">local&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budget&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">currency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50.00&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># $50/day hard cap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">24h&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reject &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># -&amp;gt; 429&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends: # failover&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">primary first, fallback second&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">primary&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fallback&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-sonnet-5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># only used when primary is unhealthy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The hard‑spend‑limit behavior — model cost catalogs, dollar/token budgets,
and a real &lt;code>429&lt;/code> on exhaustion — is walked through in
&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">AI budgets &amp;amp; hard spend limits&lt;/a>,
and the &amp;ldquo;how much headroom do you actually have&amp;rdquo; analysis is in
&lt;a href="https://maniak.io/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/">Headroom: MCP token stacking&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="7-full-observability-opentelemetry--tls">7. Full observability (OpenTelemetry / TLS)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> You cannot govern what you cannot see. With agents, the
questions are specific and expensive to answer without instrumentation:
&lt;em>What prompt was sent? What did it cost? Which model, which tool, which
agent? Where did latency come from — the model, the tool call, or the
gateway?&lt;/em> If observability is bolted on per app, you get inconsistent,
non‑comparable data. If it lives in the gateway, every request — LLM, MCP,
A2A — emits the same &lt;strong>OpenTelemetry&lt;/strong> traces, metrics, and logs, complete
with token counts and cost, and every hop is encrypted with &lt;strong>TLS&lt;/strong>.&lt;/p>
&lt;p>This is the principle that makes the other seven &lt;em>auditable&lt;/em>. Budgets
(principle 6) are only trustworthy because token accounting is real.
Guardrail decisions (principle 4) are only defensible because they&amp;rsquo;re
logged. RBAC (principle 5) is only verifiable because you can see who called
what.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Tracing is a gateway‑level policy that exports
OTLP to any collector — Langfuse, Tempo, Jaeger, an OTel Collector fan‑out:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://otel-collector.observability.svc.cluster.local:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># token usage and cost are attached to spans automatically&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Because the gateway sits on the normalized stream from principle 1, the
spans carry &lt;code>gen_ai.*&lt;/code> attributes — model, input/output tokens, cost —
consistently across providers. The end‑to‑end Langfuse integrations
(direct OTLP and via an OTel Collector) are in
&lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-standalone-langfuse-direct-otlp/">agentgateway + Langfuse direct OTLP&lt;/a>
and &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">via the OTel Collector&lt;/a>,
and the cost dashboards built on that data are in
&lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">the cost &amp;amp; tokenomics dashboard&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="8-yaml-policydriven-config">8. YAML policy‑driven config&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The previous seven principles are only as good as the
place they&amp;rsquo;re defined. If routing, auth, guardrails, and budgets live in
application code and clicked‑together dashboards, then your governance is
un‑reviewable, un‑versioned, and un‑reproducible. Someone changes a limit in
a UI at 2am and there&amp;rsquo;s no diff, no approval, no rollback.&lt;/p>
&lt;p>Declarative &lt;strong>YAML&lt;/strong> flips that. Every policy — every route, backend,
guardrail, JWT rule, budget, and trace exporter — is text. Text goes in Git.
Git gives you pull‑request review, history, blame, environments, and
one‑command rollback. Your AI governance becomes a GitOps artifact: the same
config runs on a laptop (standalone) and in production (Kubernetes CRDs),
and &amp;ldquo;what is our policy?&amp;rdquo; has a single, diffable answer.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Everything in this post &lt;em>was&lt;/em> YAML. That&amp;rsquo;s the
point — there is no hidden imperative layer. Standalone is a single
&lt;code>config.yaml&lt;/code>; on Kubernetes the same model is expressed as Gateway API and
&lt;code>agentgateway.dev&lt;/code> CRDs, reconciled by a controller. Both are declarative,
both live in Git. The GitOps pattern for agents is in
&lt;a href="https://maniak.io/articles/2026-04-02-gitops-for-agents-deployment-and-management/">GitOps for agents&lt;/a>,
and quickstarts for both surfaces are
&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">standalone&lt;/a> and
&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">Kubernetes&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="why-these-eight-together">Why these eight, together&lt;/h2>
&lt;p>Any one of these principles is useful on its own. The reason they belong in
&lt;em>one&lt;/em> gateway is that they compound:&lt;/p>
&lt;ul>
&lt;li>Unified access (1) gives you &lt;strong>one normalized stream&lt;/strong> to apply
everything else to.&lt;/li>
&lt;li>MCP federation (2) and A2A (3) put &lt;strong>tools and agent handoffs&lt;/strong> on that
same stream.&lt;/li>
&lt;li>Guardrails (4) and multi‑auth (5) turn the stream into a &lt;strong>policy
enforcement point&lt;/strong> — content and identity.&lt;/li>
&lt;li>Budgets and failover (6) make it &lt;strong>resilient and financially safe&lt;/strong>.&lt;/li>
&lt;li>Observability (7) makes all of it &lt;strong>auditable and debuggable&lt;/strong>.&lt;/li>
&lt;li>YAML (8) makes the whole thing &lt;strong>reviewable, versioned, and portable&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Pull any one out and you reopen a gap: no observability and your budgets are
guesswork; no unified access and your policies fork per provider; no YAML
and none of it is reproducible. That&amp;rsquo;s why &amp;ldquo;an API gateway with an LLM
route&amp;rdquo; isn&amp;rsquo;t enough — the AI‑native concerns (tokens, sessions, tools,
agents, cost) have to be first‑class.&lt;/p>
&lt;h2 id="where-to-start">Where to start&lt;/h2>
&lt;p>You don&amp;rsquo;t have to adopt all eight on day one. A sane path:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Front your LLM calls&lt;/strong> through the gateway (principle 1) — kill the
scattered API keys first.&lt;/li>
&lt;li>&lt;strong>Turn on observability&lt;/strong> (principle 7) — you can&amp;rsquo;t tune what you can&amp;rsquo;t
see.&lt;/li>
&lt;li>&lt;strong>Add budgets and rate limits&lt;/strong> (principle 6) — cap the downside.&lt;/li>
&lt;li>&lt;strong>Federate your MCP tools&lt;/strong> (principle 2) and put &lt;strong>guardrails +
auth&lt;/strong> (4, 5) on them.&lt;/li>
&lt;li>&lt;strong>Bring in A2A&lt;/strong> (principle 3) as your agents start delegating.&lt;/li>
&lt;li>&lt;strong>Commit all of it to Git&lt;/strong> (principle 8) from the very first step.&lt;/li>
&lt;/ol>
&lt;p>agentgateway is open source and CNCF — you can run the whole thing on a
laptop today. When you need the enterprise hardening (the F5 AI Guardrails
integration, the Solo Enterprise UI, cost management, supported CRDs), Solo
packages it as &lt;strong>Enterprise agentgateway&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>Project: &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/li>
&lt;li>Docs: &lt;a href="https://agentgateway.dev/docs/">https://agentgateway.dev/docs/&lt;/a>&lt;/li>
&lt;li>Solo.io: &lt;a href="https://www.solo.io/">https://www.solo.io/&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>The gateway era of microservices taught us that governance can&amp;rsquo;t be
per‑service and after‑the‑fact. Agents raise the stakes — the blast radius
now includes your data, your budget, and your users. These eight principles
are how you keep that in bounds.&lt;/p></description><content:encoded>&lt;p>You wouldn&amp;rsquo;t put a hundred microservices into production with no gateway,
no auth, no rate limits, and no logs. Yet that is exactly how most teams
ship AI agents today: an API key baked into a container, a direct line to
an LLM, and a growing pile of MCP tool servers that nobody is governing.&lt;/p>
&lt;p>Agents don&amp;rsquo;t fail the way microservices fail. A microservice returns a 500
and you page someone. An agent quietly sends your customer table to a
third‑party model, loops a thousand times on a bad prompt and burns your
monthly budget before lunch, or discovers a tool you never meant to expose
and happily calls it. The failure modes are new, so the control plane has
to be new too.&lt;/p>
&lt;p>That is what an &lt;strong>AI gateway&lt;/strong> is for. Not &amp;ldquo;an API gateway with an LLM
route&amp;rdquo; — a purpose‑built data plane that understands LLM traffic, MCP
sessions, and agent‑to‑agent calls, and applies policy to all three.&lt;/p>
&lt;p>This post walks through the &lt;strong>eight principles&lt;/strong> an AI gateway has to get
right, why each one matters in production, and how
&lt;a href="https://github.com/agentgateway/agentgateway">&lt;strong>agentgateway&lt;/strong>&lt;/a> — the
open‑source (CNCF) project, packaged and hardened by
&lt;a href="https://www.solo.io/">&lt;strong>Solo.io&lt;/strong>&lt;/a> as Enterprise agentgateway — implements
them. Every principle gets a &lt;em>why&lt;/em> and a &lt;em>how&lt;/em>, with config you can read.&lt;/p>
&lt;p>If you want the &amp;ldquo;why does this exist at all&amp;rdquo; version first, start with
&lt;a href="https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/">What is agentgateway.dev?&lt;/a>
and &lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">Why your AI agents need a gateway&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="the-eight-principles-at-a-glance">The eight principles at a glance&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Principle&lt;/th>
&lt;th>The problem it solves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>&lt;strong>Unified LLM access&lt;/strong> (one API, any provider)&lt;/td>
&lt;td>Every provider has a different SDK, auth, and payload. Apps get locked to one vendor.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>&lt;strong>MCP tool federation&lt;/strong> (stdio/HTTP/SSE/OpenAPI)&lt;/td>
&lt;td>Each tool server is a separate connection, auth surface, and thing to secure.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;strong>Secure A2A agent discovery &amp;amp; collaboration&lt;/strong>&lt;/td>
&lt;td>Agents calling agents with no identity, no scoping, no audit.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>&lt;strong>Built‑in guardrails&lt;/strong> (regex / moderation / webhooks)&lt;/td>
&lt;td>PII, secrets, and prompt injection flow straight through to the model.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>&lt;strong>Multi‑auth security&lt;/strong> (JWT / OAuth / RBAC / CEL)&lt;/td>
&lt;td>One coarse API key can do everything. No per‑user, per‑tool authorization.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>&lt;strong>Rate limiting, budgets &amp;amp; failover routing&lt;/strong>&lt;/td>
&lt;td>One runaway loop drains the budget; one provider outage kills every agent.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>&lt;strong>Full observability&lt;/strong> (OpenTelemetry / TLS)&lt;/td>
&lt;td>No traces, no token accounting, no idea what a prompt cost or where it went.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>&lt;strong>YAML policy‑driven config&lt;/strong>&lt;/td>
&lt;td>Governance living in app code and dashboards can&amp;rsquo;t be reviewed, versioned, or rolled back.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>None of these is optional at scale. Skip one and it becomes the incident.
Let&amp;rsquo;s take them one at a time.&lt;/p>
&lt;hr>
&lt;h2 id="1-unified-llm-access--one-api-any-provider">1. Unified LLM access — one API, any provider&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The moment you have more than one model provider — and
you will, the day one provider has an outage or another ships something
better — you feel the tax. Anthropic, OpenAI, Gemini, Bedrock, Vertex, and
your self‑hosted vLLM each have their own SDK, auth headers, request shape,
streaming semantics, and error codes. If each application wires directly to
each provider, you&amp;rsquo;ve hard‑coded vendor lock‑in into every service, and
swapping a model means a code change and a redeploy.&lt;/p>
&lt;p>An AI gateway makes this a routing concern. Apps speak &lt;strong>one&lt;/strong> stable API to
the gateway — the widely‑adopted OpenAI‑style schema is the common wire
format, but the point isn&amp;rsquo;t OpenAI, it&amp;rsquo;s &lt;em>one&lt;/em> front door — and the gateway
translates to whatever provider sits behind the route: Anthropic today,
Gemini tomorrow, a local model for the cheap path. Changing or mixing models
becomes a config change, not a code change, and no application is coupled to
any single vendor.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> A backend declares the provider; a route maps
a path prefix to that backend and normalizes the API surface. Here&amp;rsquo;s a
single standalone config front‑ending both Anthropic and OpenAI behind one
port:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4001&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Your application points &lt;code>OPENAI_BASE_URL&lt;/code> (or &lt;code>ANTHROPIC_BASE_URL&lt;/code>) at the
gateway and never holds a provider key again. On Kubernetes the same idea
is expressed as an &lt;code>AgentgatewayBackend&lt;/code> with an &lt;code>ai.provider&lt;/code> block and an
&lt;code>HTTPRoute&lt;/code> — see
&lt;a href="https://maniak.io/articles/2026-02-11-your-first-ai-route-connecting-to-openai-opensource/">Your first AI route&lt;/a>.
The payoff shows up later: because everything is normalized here, principles
6 (failover), 7 (token accounting), and 8 (policy) get to work on a single,
consistent stream instead of N provider dialects.&lt;/p>
&lt;hr>
&lt;h2 id="2-mcp-tool-federation-stdio--http--sse--openapi">2. MCP tool federation (stdio / HTTP / SSE / OpenAPI)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The &lt;a href="https://modelcontextprotocol.io/">Model Context Protocol&lt;/a>
is how agents get hands — GitHub, Slack, a database, an internal API. But
MCP servers multiply fast, and each one is a separate connection, a
separate auth story, and a separate thing to monitor. Worse, MCP is not
plain request/response: it&amp;rsquo;s &lt;strong>stateful JSON‑RPC over long‑lived sessions&lt;/strong>,
with server‑initiated SSE events that must route back to the &lt;em>correct&lt;/em>
client session. A path‑based reverse proxy can&amp;rsquo;t do that correctly.&lt;/p>
&lt;p>Federation means the gateway presents &lt;strong>one&lt;/strong> MCP endpoint to the agent and
multiplexes it across many backend tool servers — regardless of whether a
given server speaks stdio, streamable HTTP, SSE, or is really an OpenAPI
service wrapped as MCP. The agent sees one tool catalog; the gateway owns
the fan‑out, the session affinity, and the per‑target security.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> A single MCP listener with multiple targets,
each using whatever transport the server actually speaks:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">everything&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stdio&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cmd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;exec&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-i&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;mcp-everything&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;node&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;dist/index.js&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp.tools.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">internal-api&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openapi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/config/openapi/orders.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">orders.internal.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The agent connects once; agentgateway keeps the JSON‑RPC session coherent,
routes SSE events back to the right client, and — critically — lets you
decide &lt;em>which&lt;/em> tools each caller may even see. That last part is where
federation meets authorization (principle 5). For the deep version of
multiplexing and how tool exposure affects token cost, see
&lt;a href="https://maniak.io/articles/2026-02-20-mcp-multiplexing-tool-access-agentgateway/">MCP multiplexing&lt;/a>
and &lt;a href="https://maniak.io/articles/2026-06-20-github-mcp-token-economics-agentgateway-tool-modes/">GitHub MCP token economics&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="3-secure-a2a-agent-discovery--collaboration">3. Secure A2A agent discovery &amp;amp; collaboration&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The next step past &amp;ldquo;agent calls tools&amp;rdquo; is &amp;ldquo;agent calls
agent.&amp;rdquo; A planner delegates to a researcher; the researcher calls a
summarizer. &lt;a href="https://a2a-protocol.org/">A2A&lt;/a> standardizes that handoff. But
without a gateway in the middle, agent‑to‑agent traffic is the wild west:
no shared identity, no way to scope what one agent may ask another to do,
and no audit trail when a delegated call does something expensive or
sensitive. Multi‑agent systems fail &lt;em>between&lt;/em> the agents, and that seam is
exactly where nobody is looking.&lt;/p>
&lt;p>An AI gateway treats A2A as first‑class traffic: agents are discoverable
through the gateway, every cross‑agent call carries identity, and the same
auth, rate‑limit, and observability policies that guard LLM and MCP traffic
also guard the handoffs. The blast radius of a misbehaving agent stays
contained because there&amp;rsquo;s a kill switch and a policy boundary at the seam.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> A2A is routed and secured like any other
backend — an A2A route carries JWT identity forward, applies RBAC on what
the calling agent is allowed to invoke, and emits the same traces as
everything else. The multi‑agent kill‑switch pattern (revoke one agent&amp;rsquo;s
access at the gateway and it&amp;rsquo;s instantly cut off from peers and tools) is
covered in
&lt;a href="https://maniak.io/articles/2026-02-21-multi-agent-architecture-agentgateway-kill-switch/">Multi‑agent architecture + kill switch&lt;/a>,
and a full A2A/MCP stack running on kagent is in
&lt;a href="https://maniak.io/articles/2026-07-11-agentx-spacexai-x-mcp-agentgateway-kagent/">AgentX&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="4-builtin-guardrails-regex--moderation--webhooks">4. Built‑in guardrails (regex / moderation / webhooks)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> This is the principle that keeps security teams up at
night. Between the user and the model — in both directions — you need a
decision point that can &lt;em>see&lt;/em> the content and act on it: redact a credit
card, block a prompt‑injection attempt, drop a response that leaks an
internal hostname, or send the payload to an external moderation service and
honor its verdict. Bolt this into every app and you get inconsistent
coverage and a dozen places to audit. It belongs in the gateway, inline on
the request and response path.&lt;/p>
&lt;p>Guardrails come in layers, and a good gateway supports all three:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Regex / pattern&lt;/strong> rules for the cheap, deterministic cases (SSNs,
API‑key shapes, email addresses).&lt;/li>
&lt;li>&lt;strong>Moderation&lt;/strong> via a model or classification service for the fuzzy cases
(toxicity, jailbreaks, category policy).&lt;/li>
&lt;li>&lt;strong>Webhook&lt;/strong> callouts to an external decision engine — e.g. &lt;strong>F5 AI
Guardrails&lt;/strong> — for enterprise‑grade scanning, redaction, and audit,
either inline (block before it reaches the model) or out‑of‑band.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Guardrail policies attach to a route and run
on the prompt and/or the completion:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;\b\d{3}-\d{2}-\d{4}\b&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># US SSN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reject&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails.security.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;(?i)internal[- ]only&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mask&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The point is that a blocked request returns a clean, inspectable decision —
security sees &lt;em>why&lt;/em> it was blocked, the app sees a status code, and nothing
leaked. The full inline + out‑of‑band F5 pattern is in
&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/">agentgateway + F5 AI Guardrails architectures&lt;/a>
and the hands‑on UI walkthrough is
&lt;a href="https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/">here&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="5-multiauth-security-jwt--oauth--rbac--cel">5. Multi‑auth security (JWT / OAuth / RBAC / CEL)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> A single API key is a skeleton key: whoever holds it can
do everything the key can do. That&amp;rsquo;s fine for a demo and catastrophic in
production. Real systems need &lt;em>who&lt;/em> (authentication), &lt;em>what are they allowed
to do&lt;/em> (authorization), and the ability to express that at the granularity
of &amp;ldquo;this team&amp;rsquo;s agents may call these three MCP tools and this model, but
not that one.&amp;rdquo; Coarse keys can&amp;rsquo;t express that. You need identity flowing
through the gateway and policy evaluated per request.&lt;/p>
&lt;p>An AI gateway should support:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>JWT&lt;/strong> validation — verify tokens, check issuer/audience, extract claims.&lt;/li>
&lt;li>&lt;strong>OAuth&lt;/strong> — front the gateway with an IdP (Okta, Keycloak, Entra) so
agents authenticate as real principals.&lt;/li>
&lt;li>&lt;strong>RBAC&lt;/strong> — map identities/claims to roles, and roles to what they may
reach.&lt;/li>
&lt;li>&lt;strong>CEL&lt;/strong> (&lt;a href="https://cel.dev/">Common Expression Language&lt;/a>) — fine‑grained,
attribute‑based rules like &amp;ldquo;allow only if &lt;code>jwt.groups&lt;/code> contains
&lt;code>platform&lt;/code> &lt;strong>and&lt;/strong> the requested tool isn&amp;rsquo;t in the destructive set.&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> JWT auth is a policy on the listener or route;
RBAC and CEL express which authenticated principals may reach which backends
or MCP tools:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwtAuth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://login.example.com/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://login.example.com/.well-known/jwks.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># only the platform group may use write-capable tools&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s1">&amp;#39;jwt.groups.exists(g, g == &amp;#34;platform&amp;#34;) || !request.mcp.tool.startsWith(&amp;#34;create_&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Identity established at the gateway then travels &lt;em>with&lt;/em> the request into
MCP and A2A calls (principles 2 and 3), so authorization is consistent end
to end instead of re‑invented per hop. For a full OAuth chain into a cloud
runtime, see
&lt;a href="https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/">AWS AgentCore with agentgateway and Okta OAuth&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="6-rate-limiting-budgets--failover-routing">6. Rate limiting, budgets &amp;amp; failover routing&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> LLM traffic has a property normal traffic doesn&amp;rsquo;t:
&lt;strong>every request costs money&lt;/strong>, and a bad one can cost a lot. An agent stuck
in a reasoning loop doesn&amp;rsquo;t crash — it spends. So the gateway needs two
knobs traditional gateways never had:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Budgets&lt;/strong> — a hard ceiling in &lt;em>dollars or tokens&lt;/em> per user, per team,
per agent. When it&amp;rsquo;s hit, the gateway returns &lt;code>429&lt;/code>, not a surprise
invoice. This is FinOps as a policy, not a monthly spreadsheet.&lt;/li>
&lt;li>&lt;strong>Failover routing&lt;/strong> — provider A is the primary; on error, timeout, or
overload, the gateway transparently fails over to provider B (or a
cheaper/local model) without the app knowing. One outage stops being one
outage for every agent.&lt;/li>
&lt;/ul>
&lt;p>Plus classic &lt;strong>rate limiting&lt;/strong> (requests per second per principal) to
protect both your budget and the upstream provider&amp;rsquo;s limits.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Budgets attach a cost model and a ceiling; the
gateway meters real token usage against it. Failover is expressed as an
ordered/weighted backend list with health‑aware routing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">local&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budget&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">currency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50.00&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># $50/day hard cap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">24h&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">reject &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># -&amp;gt; 429&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends: # failover&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">primary first, fallback second&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">primary&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fallback&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-sonnet-5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">weight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># only used when primary is unhealthy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The hard‑spend‑limit behavior — model cost catalogs, dollar/token budgets,
and a real &lt;code>429&lt;/code> on exhaustion — is walked through in
&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">AI budgets &amp;amp; hard spend limits&lt;/a>,
and the &amp;ldquo;how much headroom do you actually have&amp;rdquo; analysis is in
&lt;a href="https://maniak.io/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/">Headroom: MCP token stacking&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="7-full-observability-opentelemetry--tls">7. Full observability (OpenTelemetry / TLS)&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> You cannot govern what you cannot see. With agents, the
questions are specific and expensive to answer without instrumentation:
&lt;em>What prompt was sent? What did it cost? Which model, which tool, which
agent? Where did latency come from — the model, the tool call, or the
gateway?&lt;/em> If observability is bolted on per app, you get inconsistent,
non‑comparable data. If it lives in the gateway, every request — LLM, MCP,
A2A — emits the same &lt;strong>OpenTelemetry&lt;/strong> traces, metrics, and logs, complete
with token counts and cost, and every hop is encrypted with &lt;strong>TLS&lt;/strong>.&lt;/p>
&lt;p>This is the principle that makes the other seven &lt;em>auditable&lt;/em>. Budgets
(principle 6) are only trustworthy because token accounting is real.
Guardrail decisions (principle 4) are only defensible because they&amp;rsquo;re
logged. RBAC (principle 5) is only verifiable because you can see who called
what.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Tracing is a gateway‑level policy that exports
OTLP to any collector — Langfuse, Tempo, Jaeger, an OTel Collector fan‑out:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://otel-collector.observability.svc.cluster.local:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># token usage and cost are attached to spans automatically&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Because the gateway sits on the normalized stream from principle 1, the
spans carry &lt;code>gen_ai.*&lt;/code> attributes — model, input/output tokens, cost —
consistently across providers. The end‑to‑end Langfuse integrations
(direct OTLP and via an OTel Collector) are in
&lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-standalone-langfuse-direct-otlp/">agentgateway + Langfuse direct OTLP&lt;/a>
and &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">via the OTel Collector&lt;/a>,
and the cost dashboards built on that data are in
&lt;a href="https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/">the cost &amp;amp; tokenomics dashboard&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="8-yaml-policydriven-config">8. YAML policy‑driven config&lt;/h2>
&lt;p>&lt;strong>Why it matters.&lt;/strong> The previous seven principles are only as good as the
place they&amp;rsquo;re defined. If routing, auth, guardrails, and budgets live in
application code and clicked‑together dashboards, then your governance is
un‑reviewable, un‑versioned, and un‑reproducible. Someone changes a limit in
a UI at 2am and there&amp;rsquo;s no diff, no approval, no rollback.&lt;/p>
&lt;p>Declarative &lt;strong>YAML&lt;/strong> flips that. Every policy — every route, backend,
guardrail, JWT rule, budget, and trace exporter — is text. Text goes in Git.
Git gives you pull‑request review, history, blame, environments, and
one‑command rollback. Your AI governance becomes a GitOps artifact: the same
config runs on a laptop (standalone) and in production (Kubernetes CRDs),
and &amp;ldquo;what is our policy?&amp;rdquo; has a single, diffable answer.&lt;/p>
&lt;p>&lt;strong>How agentgateway does it.&lt;/strong> Everything in this post &lt;em>was&lt;/em> YAML. That&amp;rsquo;s the
point — there is no hidden imperative layer. Standalone is a single
&lt;code>config.yaml&lt;/code>; on Kubernetes the same model is expressed as Gateway API and
&lt;code>agentgateway.dev&lt;/code> CRDs, reconciled by a controller. Both are declarative,
both live in Git. The GitOps pattern for agents is in
&lt;a href="https://maniak.io/articles/2026-04-02-gitops-for-agents-deployment-and-management/">GitOps for agents&lt;/a>,
and quickstarts for both surfaces are
&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/">standalone&lt;/a> and
&lt;a href="https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/">Kubernetes&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="why-these-eight-together">Why these eight, together&lt;/h2>
&lt;p>Any one of these principles is useful on its own. The reason they belong in
&lt;em>one&lt;/em> gateway is that they compound:&lt;/p>
&lt;ul>
&lt;li>Unified access (1) gives you &lt;strong>one normalized stream&lt;/strong> to apply
everything else to.&lt;/li>
&lt;li>MCP federation (2) and A2A (3) put &lt;strong>tools and agent handoffs&lt;/strong> on that
same stream.&lt;/li>
&lt;li>Guardrails (4) and multi‑auth (5) turn the stream into a &lt;strong>policy
enforcement point&lt;/strong> — content and identity.&lt;/li>
&lt;li>Budgets and failover (6) make it &lt;strong>resilient and financially safe&lt;/strong>.&lt;/li>
&lt;li>Observability (7) makes all of it &lt;strong>auditable and debuggable&lt;/strong>.&lt;/li>
&lt;li>YAML (8) makes the whole thing &lt;strong>reviewable, versioned, and portable&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Pull any one out and you reopen a gap: no observability and your budgets are
guesswork; no unified access and your policies fork per provider; no YAML
and none of it is reproducible. That&amp;rsquo;s why &amp;ldquo;an API gateway with an LLM
route&amp;rdquo; isn&amp;rsquo;t enough — the AI‑native concerns (tokens, sessions, tools,
agents, cost) have to be first‑class.&lt;/p>
&lt;h2 id="where-to-start">Where to start&lt;/h2>
&lt;p>You don&amp;rsquo;t have to adopt all eight on day one. A sane path:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Front your LLM calls&lt;/strong> through the gateway (principle 1) — kill the
scattered API keys first.&lt;/li>
&lt;li>&lt;strong>Turn on observability&lt;/strong> (principle 7) — you can&amp;rsquo;t tune what you can&amp;rsquo;t
see.&lt;/li>
&lt;li>&lt;strong>Add budgets and rate limits&lt;/strong> (principle 6) — cap the downside.&lt;/li>
&lt;li>&lt;strong>Federate your MCP tools&lt;/strong> (principle 2) and put &lt;strong>guardrails +
auth&lt;/strong> (4, 5) on them.&lt;/li>
&lt;li>&lt;strong>Bring in A2A&lt;/strong> (principle 3) as your agents start delegating.&lt;/li>
&lt;li>&lt;strong>Commit all of it to Git&lt;/strong> (principle 8) from the very first step.&lt;/li>
&lt;/ol>
&lt;p>agentgateway is open source and CNCF — you can run the whole thing on a
laptop today. When you need the enterprise hardening (the F5 AI Guardrails
integration, the Solo Enterprise UI, cost management, supported CRDs), Solo
packages it as &lt;strong>Enterprise agentgateway&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>Project: &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/li>
&lt;li>Docs: &lt;a href="https://agentgateway.dev/docs/">https://agentgateway.dev/docs/&lt;/a>&lt;/li>
&lt;li>Solo.io: &lt;a href="https://www.solo.io/">https://www.solo.io/&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>The gateway era of microservices taught us that governance can&amp;rsquo;t be
per‑service and after‑the‑fact. Agents raise the stakes — the blast radius
now includes your data, your budget, and your users. These eight principles
are how you keep that in bounds.&lt;/p></content:encoded></item><item><title>Build Your Own X Agent: SpaceXAI + X MCP Through agentgateway and kagent</title><link>https://maniak.io/articles/2026-07-11-agentx-spacexai-x-mcp-agentgateway-kagent/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-11-agentx-spacexai-x-mcp-agentgateway-kagent/</guid><description>&lt;p>What if you could ask an AI agent &lt;em>&amp;ldquo;What&amp;rsquo;s trending in the US right now?&amp;rdquo;&lt;/em> or &lt;em>&amp;ldquo;Summarize the last week of posts about agentgateway&amp;rdquo;&lt;/em> — and get real answers grounded in live X (Twitter) data, not a hallucinated timeline?&lt;/p>
&lt;p>That&amp;rsquo;s &lt;strong>AgentX&lt;/strong>: a kagent agent powered by &lt;strong>SpaceXAI (xAI Grok-4.5)&lt;/strong>, tools from the &lt;strong>official X MCP server&lt;/strong> (&lt;code>api.x.com/mcp&lt;/code>), and traffic that all flows through &lt;strong>Solo Enterprise AgentGateway&lt;/strong>. Trends, news, search, users, timelines, mentions, and bookmark curation — with human-in-the-loop approval before any bookmark write lands on your account.&lt;/p>
&lt;p>Everything below is running in my lab GitOps repo:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Live map: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s AgentX in the Solo Enterprise UI — &lt;code>grok-4.5&lt;/code>, ready for research:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentx-solo-ui.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentx-solo-ui.png" alt="AgentX in Solo Enterprise for kagent — chat UI with agentx / grok-4.5 selected, greeting &amp;amp;lsquo;Hey Admin, Where should we begin?&amp;amp;rsquo; and example prompt &amp;amp;lsquo;What&amp;amp;rsquo;s trending in the US right now?&amp;amp;rsquo;" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="why-this-stack">Why this stack&lt;/h2>
&lt;p>X is still where news, tech discourse, and media break first. Scraping it yourself is fragile. Sticking an API key in a notebook agent is worse — no audit trail, no rate-limit story, no shared gateway.&lt;/p>
&lt;p>The pattern I want for every agent in the lab is the same one I used for &lt;a href="https://maniak.io/articles/2026-06-20-github-mcp-token-economics-agentgateway-tool-modes/">GitHub MCP&lt;/a>, &lt;a href="https://maniak.io/articles/2026-03-15-fortigate-firewall-telegram-kagent-mcp/">FortiGate&lt;/a>, and the &lt;a href="https://maniak.io/articles/2026-07-11-telegram-multi-agent-bot-kagent-agentgateway/">Telegram multi-agent bot&lt;/a>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>SpaceXAI (Grok-4.5)&lt;/strong>&lt;/td>
&lt;td>The brain — strong at social tone, trends, and summarization&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Official X MCP&lt;/strong>&lt;/td>
&lt;td>The hands — tools against &lt;code>api.x.com/mcp&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>agentgateway&lt;/strong>&lt;/td>
&lt;td>The front door — MCP proxy at &lt;code>/x&lt;/code>, LLM proxy at &lt;code>/grok&lt;/code>, traces + cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent AgentX&lt;/strong>&lt;/td>
&lt;td>The product surface — system prompt, tool allowlist, HITL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Vault + ESO&lt;/strong>&lt;/td>
&lt;td>Secrets never live in Git&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>You talk to the agent. The agent never holds an X API key &lt;em>or&lt;/em> an xAI key in plain YAML. Credentials stay in Vault; the OAuth user token stays on a PVC inside a bridge pod that only agentgateway can reach.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>The official X MCP bridge (&lt;code>@xdevplatform/xurl&lt;/code>) speaks &lt;strong>stdio&lt;/strong>. agentgateway and kagent expect &lt;strong>Streamable HTTP MCP&lt;/strong>. So we wrap stdio with &lt;strong>supergateway&lt;/strong>, then front the whole thing with the virtual MCP gateway — same shape as every other MCP server in &lt;a href="https://github.com/sebbycorp/k8s-goose">k8s-goose&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">You (Solo UI / A2A / Telegram later)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌───────────────────┐ ModelConfig xai-grok
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ kagent AgentX │ ──────────────────────────▶ xai-grok-gateway ──▶ api.x.ai (Grok-4.5)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (ns: kagent) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────┬─────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ RemoteMCPServer x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌───────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ virtual-mcp- │ HTTPRoute /x
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ gateway │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────┬─────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ AgentgatewayBackend x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌───────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ x-mcp-bridge pod │ supergateway --stdio &amp;#34;xurl mcp https://api.x.com/mcp&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ :8080/mcp │ HOME=/data → PVC (OAuth token)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ CLIENT_ID/SECRET │ ← Secret x-app-credentials (ESO ← Vault agentgateway/x)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────┬─────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> api.x.com/mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Important limitation (as of this write-up):&lt;/strong> the official X MCP surface AgentX uses is &lt;strong>read + bookmarks&lt;/strong>. Trends, news, search, post/user lookup, timelines, mentions, and bookmark folders work. &lt;strong>Posting, replying, liking, and media upload are not exposed by this MCP server.&lt;/strong> AgentX is honest about that in its system prompt — it will draft text for you and bookmark sources, but it will not pretend it can tweet.&lt;/p>
&lt;hr>
&lt;h2 id="what-agentx-can-do">What AgentX can do&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Tools (subset)&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Trends &amp;amp; news&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_trends_by_woeid&lt;/code>, &lt;code>search_news&lt;/code>, &lt;code>get_news&lt;/code>&lt;/td>
&lt;td>WOEID &lt;code>1&lt;/code> = worldwide, &lt;code>23424977&lt;/code> = USA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Posts &amp;amp; search&lt;/strong>&lt;/td>
&lt;td>&lt;code>search_posts_all&lt;/code>, &lt;code>get_posts_by_id(s)&lt;/code>, &lt;code>get_posts_counts_recent&lt;/code>, engagement tools&lt;/td>
&lt;td>Full-archive search needs a paid X API tier&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Users&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_users_me&lt;/code>, &lt;code>get_users_by_username(s)&lt;/code>, &lt;code>search_users&lt;/code>&lt;/td>
&lt;td>Research accounts cleanly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Timelines&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_users_timeline&lt;/code>, &lt;code>get_users_posts&lt;/code>, &lt;code>get_users_mentions&lt;/code>&lt;/td>
&lt;td>Home timeline + mentions for the OAuth user&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Bookmarks (write)&lt;/strong>&lt;/td>
&lt;td>&lt;code>create_users_bookmark&lt;/code>, &lt;code>delete_users_bookmark&lt;/code>, &lt;code>create_users_bookmark_folder&lt;/code>&lt;/td>
&lt;td>&lt;strong>HITL required&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Bookmarks (read)&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_users_bookmarks&lt;/code>, folders, by-folder&lt;/td>
&lt;td>Safe curation views&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Example prompts that already work in the UI:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;What&amp;rsquo;s trending in the US right now?&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Search recent posts about AI agents and summarize the themes&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Look up @SebbyCorp and summarize their recent posts&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;How many posts in the last week mention &amp;lsquo;agentgateway&amp;rsquo;?&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Create a bookmark folder called &amp;lsquo;AI agents&amp;rsquo; and save these posts&amp;rdquo;&lt;/em>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>You need a cluster where &lt;strong>agentgateway&lt;/strong> and &lt;strong>kagent&lt;/strong> are already healthy (or follow the full platform deploy in &lt;a href="https://github.com/sebbycorp/k8s-goose">k8s-goose&lt;/a>). For AgentX specifically:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>X Developer app&lt;/strong> at &lt;a href="https://console.x.com">console.x.com&lt;/a> with OAuth 2.0
&lt;ul>
&lt;li>App permissions: &lt;strong>Read and write&lt;/strong> (bookmarks need write)&lt;/li>
&lt;li>Callback URI: &lt;code>http://localhost:8080/callback&lt;/code>&lt;/li>
&lt;li>Paid tier (Basic+) if you want full search / archive behavior&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>SpaceXAI / xAI API key&lt;/strong> already wired through agentgateway (ModelConfig &lt;code>xai-grok&lt;/code>)&lt;/li>
&lt;li>&lt;strong>Vault + External Secrets Operator&lt;/strong> (or a plain Secret if you&amp;rsquo;re not on Vault yet)&lt;/li>
&lt;li>A storage class for a &lt;strong>1Gi RWO PVC&lt;/strong> (Longhorn in my lab)&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="part-1--x-app-credentials-in-vault--kubernetes">Part 1 — X app credentials in Vault → Kubernetes&lt;/h2>
&lt;p>Never commit client secrets. Seed Vault, let ESO materialize the Secret the bridge will mount.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Example only — use YOUR client_id / client_secret from console.x.com&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -i -n vault vault-0 -- vault kv put agentgateway/x &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">client_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;YOUR_CLIENT_ID&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">client_secret&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;YOUR_CLIENT_SECRET&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ExternalSecret (from &lt;code>config/external-secrets/x-app-external-secret.yaml&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refreshInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1h&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">creationPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Owner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway/x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_SECRET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway/x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client_secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After sync:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system get secret x-app-credentials
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you use the reboot-safe Vault helper in k8s-goose, the same path is re-seeded by &lt;code>scripts/configure-vault.sh&lt;/code> so a wiped dev-mode Vault does not leave AgentX broken after a node restart.&lt;/p>
&lt;hr>
&lt;h2 id="part-2--the-x-mcp-bridge-pod-xurl--supergateway">Part 2 — The X MCP bridge pod (xurl + supergateway)&lt;/h2>
&lt;p>This is the piece that turns official X MCP into something agentgateway can proxy.&lt;/p>
&lt;p>From &lt;code>config/mcp-servers/x-mcp-server.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PersistentVolumeClaim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accessModes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;ReadWriteOnce&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storageClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">longhorn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># mandatory — RWO PVC + one OAuth owner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Recreate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:20-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> npx -y supergateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --stdio &amp;#34;npx -y @xdevplatform/xurl mcp https://api.x.com/mcp&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --outputTransport streamableHttp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --stateful
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --sessionTimeout 600000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --port 8080&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HOME&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">REDIRECT_URI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://localhost:8080/callback&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_SECRET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_SECRET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tcpSocket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu: 100m, memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu: 500m, memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">512Mi }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">persistentVolumeClaim&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">appProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why &lt;code>replicas: 1&lt;/code> and &lt;code>Recreate&lt;/code>? The OAuth refresh token lives on a &lt;strong>single RWO volume&lt;/strong> under &lt;code>/data&lt;/code> (&lt;code>HOME=/data&lt;/code> → &lt;code>~/.xurl&lt;/code>). Scaling the bridge would fight for the volume and corrupt the token owner story.&lt;/p>
&lt;hr>
&lt;h2 id="part-3--one-time-oauth-bootstrap-headless-inside-the-pod">Part 3 — One-time OAuth bootstrap (headless, inside the pod)&lt;/h2>
&lt;p>The bridge cannot mint the first user token without a browser once. Do this after the Deployment is Ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system &lt;span class="nb">exec&lt;/span> -it deploy/x-mcp-bridge -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> npx -y @xdevplatform/xurl auth oauth2 --headless
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>Open the printed authorization URL in a browser&lt;/li>
&lt;li>Authorize the X app for your account&lt;/li>
&lt;li>The browser redirects to &lt;code>http://localhost:8080/callback?...&lt;/code> (the page will fail to load — that&amp;rsquo;s fine)&lt;/li>
&lt;li>Paste the &lt;strong>full redirect URL&lt;/strong> back into the terminal&lt;/li>
&lt;/ol>
&lt;p>Confirm the token landed on the PVC and a live call works:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system &lt;span class="nb">exec&lt;/span> deploy/x-mcp-bridge -- ls -la /data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system &lt;span class="nb">exec&lt;/span> deploy/x-mcp-bridge -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sh -c &lt;span class="s1">&amp;#39;npx -y @xdevplatform/xurl whoami 2&amp;gt;/dev/null&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see your handle / user id. Refresh tokens rotate in place on the PVC — do &lt;strong>not&lt;/strong> delete that volume casually.&lt;/p>
&lt;hr>
&lt;h2 id="part-4--front-the-bridge-with-agentgateway">Part 4 — Front the bridge with agentgateway&lt;/h2>
&lt;h3 id="backend">Backend&lt;/h3>
&lt;p>&lt;code>config/backends/x-mcp.yaml&lt;/code> — no &lt;code>auth.secretRef&lt;/code>. Auth is already inside the bridge via xurl.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge.agentgateway-system.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StreamableHTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="route">Route&lt;/h3>
&lt;p>&lt;code>config/routes/x-mcp-route.yaml&lt;/code> attaches &lt;code>/x&lt;/code> to the &lt;strong>virtual MCP gateway&lt;/strong> (the same aggregator that serves &lt;code>/github&lt;/code>, &lt;code>/drone&lt;/code>, etc.):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">virtual-mcp-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Smoke-test through the gateway (adjust NodePort / IP for your cluster):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS -X POST http://&amp;lt;worker-node&amp;gt;:31606/x &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Content-Type: application/json&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Accept: application/json, text/event-stream&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;jsonrpc&amp;#34;:&amp;#34;2.0&amp;#34;,&amp;#34;id&amp;#34;:1,&amp;#34;method&amp;#34;:&amp;#34;tools/list&amp;#34;,&amp;#34;params&amp;#34;:{}}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should get a JSON-RPC &lt;code>tools&lt;/code> list with the X tool names. That&amp;rsquo;s the contract kagent will discover next.&lt;/p>
&lt;p>The Solo UI for agentgateway is the control plane for routes, cost, and playground wiring once backends exist:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentgateway-playground.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentgateway-playground.png" alt="Solo Enterprise for agentgateway — Playground / Select a Route empty state before MCP routes are programmed" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>After ArgoCD syncs the backend + HTTPRoute, &lt;code>/x&lt;/code> shows up on that virtual MCP gateway path — not as a bare LLM playground route, but as a first-class MCP target the rest of the platform can share.&lt;/p>
&lt;hr>
&lt;h2 id="part-5--spacexai-model-path-grok-through-the-gateway">Part 5 — SpaceXAI model path (Grok through the gateway)&lt;/h2>
&lt;p>AgentX does &lt;strong>not&lt;/strong> call &lt;code>api.x.ai&lt;/code> directly. kagent&amp;rsquo;s &lt;code>ModelConfig&lt;/code> points at the dedicated &lt;strong>xAI Grok gateway&lt;/strong> already in the cluster, with a throwaway client key (agentgateway injects the real &lt;code>XAI_API_KEY&lt;/code> upstream):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ModelConfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OpenAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4.5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeySecret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeySecretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">baseUrl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://xai-grok-gateway.agentgateway-system.svc.cluster.local/grok/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the SpaceXAI half of the story: &lt;strong>Grok-4.5 for reasoning&lt;/strong>, metered and traced by agentgateway, same as every other model in the lab.&lt;/p>
&lt;hr>
&lt;h2 id="part-6--kagent-remotemcpserver--agentx">Part 6 — kagent RemoteMCPServer + AgentX&lt;/h2>
&lt;h3 id="remotemcpserver">RemoteMCPServer&lt;/h3>
&lt;p>kagent must discover tools &lt;strong>through&lt;/strong> agentgateway, not by talking to the bridge Service directly. That keeps MCP traffic on the governed path (and consistent with GitHub / drone / FortiGate).&lt;/p>
&lt;p>&lt;code>config/kagent-models/x-remotemcpserver.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;X (Twitter) MCP via agentgateway /x — trends, news, post/user search &amp;amp; lookup, timelines, mentions, and bookmarks&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://virtual-mcp-gateway.agentgateway-system.svc.cluster.local/x&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;30s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sseReadTimeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5m0s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="agent-cr">Agent CR&lt;/h3>
&lt;p>&lt;code>config/kagent-models/agentx.yaml&lt;/code> (trimmed for readability — full file is in the repo):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AI agent for X (Twitter) — analyze trends and news, search and read posts/users, read timelines and mentions, and manage bookmarks&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">a2aConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">skills&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-research-curation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">X (Twitter) Research &amp;amp; Curation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Analyze trends and news, search and read posts and users, read timelines and mentions, and organize bookmarks on X&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">examples&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;What&amp;#39;s trending in the US right now?&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Search recent posts about AI agents and summarize the themes&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Bookmark this post for me: &amp;lt;post-id&amp;gt;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">x, twitter, social, trends, research]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are {{.AgentName}}, an expert X research and curation assistant.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You operate on the account authenticated in the X MCP bridge.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Capabilities
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Trends, news, search, post/user lookup, timelines, mentions, bookmarks.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Limitation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> This MCP server does NOT provide posting, replying, liking, reposting,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> or media upload. Draft text instead; offer bookmark + research workflows.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Rules
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1. Read before acting — ground claims in tool output.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2. Confirm every bookmark write.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3. Never fabricate counts or handles.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 4. Cite post IDs / links.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 5. Map locations to WOEIDs (USA = 23424977, worldwide = 1).
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 6. Batch lookups to respect X rate limits.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Available tools: {{.ToolNames}}&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_trends_by_woeid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">search_news&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_news&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">search_posts_all&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_by_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_by_ids&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_counts_recent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_liking_users&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_reposted_by&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_quoted_posts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_me&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_by_username&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_by_usernames&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_by_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">search_users&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_timeline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_posts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_mentions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_bookmarks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_bookmark_folders&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_bookmarks_by_folder_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark_folder&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark_folder&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two details that matter in production:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>runtime: python&lt;/code>&lt;/strong> — pins the published kagent app image. The default Go runtime image can &lt;code>ImagePullBackOff&lt;/code> depending on your registry digest story; every healthy agent in this lab uses Python.&lt;/li>
&lt;li>&lt;strong>&lt;code>requireApproval&lt;/code>&lt;/strong> on the three bookmark mutations — HITL at the tool layer, plus the system prompt that asks the model to confirm in natural language first.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="deploy-path-gitops">Deploy path (GitOps)&lt;/h2>
&lt;p>In k8s-goose, a push to &lt;code>main&lt;/code> is the deploy:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>File&lt;/th>
&lt;th>ArgoCD owner&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>config/external-secrets/x-app-external-secret.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/mcp-servers/x-mcp-server.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/backends/x-mcp.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/routes/x-mcp-route.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/kagent-models/x-remotemcpserver.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-models&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/kagent-models/agentx.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-models&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Order of operations:&lt;/p>
&lt;ol>
&lt;li>Seed Vault → ExternalSecret → &lt;code>x-app-credentials&lt;/code>&lt;/li>
&lt;li>Bridge Deployment + PVC + Service&lt;/li>
&lt;li>OAuth bootstrap (manual, once)&lt;/li>
&lt;li>Backend + &lt;code>/x&lt;/code> HTTPRoute&lt;/li>
&lt;li>RemoteMCPServer + AgentX&lt;/li>
&lt;li>Chat in Solo UI&lt;/li>
&lt;/ol>
&lt;p>Verify:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system get deploy,svc,pvc,agentgatewaybackend,httproute &lt;span class="p">|&lt;/span> grep x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n kagent get remotemcpserver x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n kagent get agent agentx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agent Ready, then open the kagent product surface in Solo UI, select &lt;strong>agentx&lt;/strong>, and ask:&lt;/p>
&lt;blockquote>
&lt;p>What&amp;rsquo;s trending in the US right now?&lt;/p>
&lt;/blockquote>
&lt;p>You should see a tool call to &lt;code>get_trends_by_woeid&lt;/code> (WOEID &lt;code>23424977&lt;/code>) and a clean summary — not a generic model guess.&lt;/p>
&lt;hr>
&lt;h2 id="security-and-operational-gotchas">Security and operational gotchas&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Gotcha&lt;/th>
&lt;th>Why it bites&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Scale the bridge to 2&lt;/strong>&lt;/td>
&lt;td>RWO PVC + single OAuth token&lt;/td>
&lt;td>Keep &lt;code>replicas: 1&lt;/code>, &lt;code>strategy: Recreate&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Wipe the PVC&lt;/strong>&lt;/td>
&lt;td>Refresh token is gone&lt;/td>
&lt;td>Re-run headless OAuth bootstrap&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Dev-mode Vault reboot&lt;/strong>&lt;/td>
&lt;td>&lt;code>agentgateway/x&lt;/code> disappears&lt;/td>
&lt;td>Re-run &lt;code>configure-vault.sh&lt;/code>, force-sync ExternalSecret&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Missing callback URI&lt;/strong>&lt;/td>
&lt;td>OAuth never completes&lt;/td>
&lt;td>Register &lt;code>http://localhost:8080/callback&lt;/code> on the X app&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>App-only Bearer only&lt;/strong>&lt;/td>
&lt;td>No user context / bookmarks&lt;/td>
&lt;td>Use the full xurl OAuth user token path&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secrets in chat or Git&lt;/strong>&lt;/td>
&lt;td>Rotate immediately&lt;/td>
&lt;td>Regenerate client secret in console.x.com, re-seed Vault, restart bridge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Expecting AgentX to post&lt;/strong>&lt;/td>
&lt;td>Official MCP doesn&amp;rsquo;t expose post tools&lt;/td>
&lt;td>Draft + bookmark; custom write wrapper is a planned follow-up&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Also respect X API product terms and rate limits. Batch with &lt;code>*_by_ids&lt;/code> tools when you can. This is a research/curation agent on &lt;strong>your&lt;/strong> account context — treat it like a privileged console, not a public bot.&lt;/p>
&lt;hr>
&lt;h2 id="what-this-unlocks-next">What this unlocks next&lt;/h2>
&lt;p>AgentX is deliberately the same shape as the rest of the multi-agent lab:&lt;/p>
&lt;ul>
&lt;li>Add &lt;code>@x&lt;/code> to the &lt;a href="https://maniak.io/articles/2026-07-11-telegram-multi-agent-bot-kagent-agentgateway/">Telegram multi-agent bot&lt;/a> via A2A&lt;/li>
&lt;li>Point a scheduled job at morning trend digests (agent-scheduler → PR or chat)&lt;/li>
&lt;li>Layer enterprise tool modes (Search / Code / CodeSearch) if the X tool catalog grows large enough that Standard mode burns context&lt;/li>
&lt;li>Build a &lt;strong>write&lt;/strong> MCP wrapper on top of xurl CLI for posting — behind HITL, never free-fire&lt;/li>
&lt;/ul>
&lt;p>The point of agentgateway in front of MCP is not ceremony. It&amp;rsquo;s that &lt;strong>every tool call is a first-class route&lt;/strong>: observable, shareable across agents, and isolated from the LLM key path. SpaceXAI handles the intelligence. X MCP handles the media surface. kagent turns both into something you can actually operate.&lt;/p>
&lt;hr>
&lt;h2 id="repo-map">Repo map&lt;/h2>
&lt;p>All manifests live here:&lt;/p>
&lt;ul>
&lt;li>Bridge: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/mcp-servers/x-mcp-server.yaml">&lt;code>config/mcp-servers/x-mcp-server.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>Backend: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/backends/x-mcp.yaml">&lt;code>config/backends/x-mcp.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>Route: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/routes/x-mcp-route.yaml">&lt;code>config/routes/x-mcp-route.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>RemoteMCPServer: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/kagent-models/x-remotemcpserver.yaml">&lt;code>config/kagent-models/x-remotemcpserver.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>AgentX: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/kagent-models/agentx.yaml">&lt;code>config/kagent-models/agentx.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>ExternalSecret: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/external-secrets/x-app-external-secret.yaml">&lt;code>config/external-secrets/x-app-external-secret.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Platform overview: &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Interactive docs: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="wrap-up">Wrap-up&lt;/h2>
&lt;p>You now have a repeatable pattern for a personal &lt;strong>X research agent&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>SpaceXAI Grok-4.5&lt;/strong> via agentgateway (&lt;code>xai-grok&lt;/code> ModelConfig)&lt;/li>
&lt;li>&lt;strong>Official X MCP&lt;/strong> wrapped by supergateway for Streamable HTTP&lt;/li>
&lt;li>&lt;strong>agentgateway&lt;/strong> &lt;code>/x&lt;/code> on the virtual MCP gateway&lt;/li>
&lt;li>&lt;strong>kagent AgentX&lt;/strong> with a curated tool list and HITL on bookmark writes&lt;/li>
&lt;li>&lt;strong>Vault-backed&lt;/strong> app credentials and a durable OAuth token on PVC&lt;/li>
&lt;/ol>
&lt;p>Ask it what&amp;rsquo;s trending. Ask it to research a handle. Ask it to build a bookmark folder around a topic. Keep the write surface narrow until you&amp;rsquo;re ready for a custom poster.&lt;/p>
&lt;p>That&amp;rsquo;s how you stop doomscrolling the firehose — and start running an agent that actually manages the world of news, media, and posts for you.&lt;/p></description><content:encoded>&lt;p>What if you could ask an AI agent &lt;em>&amp;ldquo;What&amp;rsquo;s trending in the US right now?&amp;rdquo;&lt;/em> or &lt;em>&amp;ldquo;Summarize the last week of posts about agentgateway&amp;rdquo;&lt;/em> — and get real answers grounded in live X (Twitter) data, not a hallucinated timeline?&lt;/p>
&lt;p>That&amp;rsquo;s &lt;strong>AgentX&lt;/strong>: a kagent agent powered by &lt;strong>SpaceXAI (xAI Grok-4.5)&lt;/strong>, tools from the &lt;strong>official X MCP server&lt;/strong> (&lt;code>api.x.com/mcp&lt;/code>), and traffic that all flows through &lt;strong>Solo Enterprise AgentGateway&lt;/strong>. Trends, news, search, users, timelines, mentions, and bookmark curation — with human-in-the-loop approval before any bookmark write lands on your account.&lt;/p>
&lt;p>Everything below is running in my lab GitOps repo:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Live map: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s AgentX in the Solo Enterprise UI — &lt;code>grok-4.5&lt;/code>, ready for research:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentx-solo-ui.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentx-solo-ui.png" alt="AgentX in Solo Enterprise for kagent — chat UI with agentx / grok-4.5 selected, greeting &amp;amp;lsquo;Hey Admin, Where should we begin?&amp;amp;rsquo; and example prompt &amp;amp;lsquo;What&amp;amp;rsquo;s trending in the US right now?&amp;amp;rsquo;" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="why-this-stack">Why this stack&lt;/h2>
&lt;p>X is still where news, tech discourse, and media break first. Scraping it yourself is fragile. Sticking an API key in a notebook agent is worse — no audit trail, no rate-limit story, no shared gateway.&lt;/p>
&lt;p>The pattern I want for every agent in the lab is the same one I used for &lt;a href="https://maniak.io/articles/2026-06-20-github-mcp-token-economics-agentgateway-tool-modes/">GitHub MCP&lt;/a>, &lt;a href="https://maniak.io/articles/2026-03-15-fortigate-firewall-telegram-kagent-mcp/">FortiGate&lt;/a>, and the &lt;a href="https://maniak.io/articles/2026-07-11-telegram-multi-agent-bot-kagent-agentgateway/">Telegram multi-agent bot&lt;/a>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>SpaceXAI (Grok-4.5)&lt;/strong>&lt;/td>
&lt;td>The brain — strong at social tone, trends, and summarization&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Official X MCP&lt;/strong>&lt;/td>
&lt;td>The hands — tools against &lt;code>api.x.com/mcp&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>agentgateway&lt;/strong>&lt;/td>
&lt;td>The front door — MCP proxy at &lt;code>/x&lt;/code>, LLM proxy at &lt;code>/grok&lt;/code>, traces + cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent AgentX&lt;/strong>&lt;/td>
&lt;td>The product surface — system prompt, tool allowlist, HITL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Vault + ESO&lt;/strong>&lt;/td>
&lt;td>Secrets never live in Git&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>You talk to the agent. The agent never holds an X API key &lt;em>or&lt;/em> an xAI key in plain YAML. Credentials stay in Vault; the OAuth user token stays on a PVC inside a bridge pod that only agentgateway can reach.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>The official X MCP bridge (&lt;code>@xdevplatform/xurl&lt;/code>) speaks &lt;strong>stdio&lt;/strong>. agentgateway and kagent expect &lt;strong>Streamable HTTP MCP&lt;/strong>. So we wrap stdio with &lt;strong>supergateway&lt;/strong>, then front the whole thing with the virtual MCP gateway — same shape as every other MCP server in &lt;a href="https://github.com/sebbycorp/k8s-goose">k8s-goose&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">You (Solo UI / A2A / Telegram later)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌───────────────────┐ ModelConfig xai-grok
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ kagent AgentX │ ──────────────────────────▶ xai-grok-gateway ──▶ api.x.ai (Grok-4.5)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (ns: kagent) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────┬─────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ RemoteMCPServer x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌───────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ virtual-mcp- │ HTTPRoute /x
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ gateway │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────┬─────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ AgentgatewayBackend x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌───────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ x-mcp-bridge pod │ supergateway --stdio &amp;#34;xurl mcp https://api.x.com/mcp&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ :8080/mcp │ HOME=/data → PVC (OAuth token)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ CLIENT_ID/SECRET │ ← Secret x-app-credentials (ESO ← Vault agentgateway/x)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────┬─────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> api.x.com/mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Important limitation (as of this write-up):&lt;/strong> the official X MCP surface AgentX uses is &lt;strong>read + bookmarks&lt;/strong>. Trends, news, search, post/user lookup, timelines, mentions, and bookmark folders work. &lt;strong>Posting, replying, liking, and media upload are not exposed by this MCP server.&lt;/strong> AgentX is honest about that in its system prompt — it will draft text for you and bookmark sources, but it will not pretend it can tweet.&lt;/p>
&lt;hr>
&lt;h2 id="what-agentx-can-do">What AgentX can do&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Tools (subset)&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Trends &amp;amp; news&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_trends_by_woeid&lt;/code>, &lt;code>search_news&lt;/code>, &lt;code>get_news&lt;/code>&lt;/td>
&lt;td>WOEID &lt;code>1&lt;/code> = worldwide, &lt;code>23424977&lt;/code> = USA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Posts &amp;amp; search&lt;/strong>&lt;/td>
&lt;td>&lt;code>search_posts_all&lt;/code>, &lt;code>get_posts_by_id(s)&lt;/code>, &lt;code>get_posts_counts_recent&lt;/code>, engagement tools&lt;/td>
&lt;td>Full-archive search needs a paid X API tier&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Users&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_users_me&lt;/code>, &lt;code>get_users_by_username(s)&lt;/code>, &lt;code>search_users&lt;/code>&lt;/td>
&lt;td>Research accounts cleanly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Timelines&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_users_timeline&lt;/code>, &lt;code>get_users_posts&lt;/code>, &lt;code>get_users_mentions&lt;/code>&lt;/td>
&lt;td>Home timeline + mentions for the OAuth user&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Bookmarks (write)&lt;/strong>&lt;/td>
&lt;td>&lt;code>create_users_bookmark&lt;/code>, &lt;code>delete_users_bookmark&lt;/code>, &lt;code>create_users_bookmark_folder&lt;/code>&lt;/td>
&lt;td>&lt;strong>HITL required&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Bookmarks (read)&lt;/strong>&lt;/td>
&lt;td>&lt;code>get_users_bookmarks&lt;/code>, folders, by-folder&lt;/td>
&lt;td>Safe curation views&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Example prompts that already work in the UI:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;What&amp;rsquo;s trending in the US right now?&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Search recent posts about AI agents and summarize the themes&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Look up @SebbyCorp and summarize their recent posts&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;How many posts in the last week mention &amp;lsquo;agentgateway&amp;rsquo;?&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Create a bookmark folder called &amp;lsquo;AI agents&amp;rsquo; and save these posts&amp;rdquo;&lt;/em>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>You need a cluster where &lt;strong>agentgateway&lt;/strong> and &lt;strong>kagent&lt;/strong> are already healthy (or follow the full platform deploy in &lt;a href="https://github.com/sebbycorp/k8s-goose">k8s-goose&lt;/a>). For AgentX specifically:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>X Developer app&lt;/strong> at &lt;a href="https://console.x.com">console.x.com&lt;/a> with OAuth 2.0
&lt;ul>
&lt;li>App permissions: &lt;strong>Read and write&lt;/strong> (bookmarks need write)&lt;/li>
&lt;li>Callback URI: &lt;code>http://localhost:8080/callback&lt;/code>&lt;/li>
&lt;li>Paid tier (Basic+) if you want full search / archive behavior&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>SpaceXAI / xAI API key&lt;/strong> already wired through agentgateway (ModelConfig &lt;code>xai-grok&lt;/code>)&lt;/li>
&lt;li>&lt;strong>Vault + External Secrets Operator&lt;/strong> (or a plain Secret if you&amp;rsquo;re not on Vault yet)&lt;/li>
&lt;li>A storage class for a &lt;strong>1Gi RWO PVC&lt;/strong> (Longhorn in my lab)&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="part-1--x-app-credentials-in-vault--kubernetes">Part 1 — X app credentials in Vault → Kubernetes&lt;/h2>
&lt;p>Never commit client secrets. Seed Vault, let ESO materialize the Secret the bridge will mount.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Example only — use YOUR client_id / client_secret from console.x.com&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -i -n vault vault-0 -- vault kv put agentgateway/x &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">client_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;YOUR_CLIENT_ID&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">client_secret&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;YOUR_CLIENT_SECRET&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ExternalSecret (from &lt;code>config/external-secrets/x-app-external-secret.yaml&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refreshInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1h&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">creationPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Owner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway/x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_SECRET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway/x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">client_secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After sync:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system get secret x-app-credentials
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you use the reboot-safe Vault helper in k8s-goose, the same path is re-seeded by &lt;code>scripts/configure-vault.sh&lt;/code> so a wiped dev-mode Vault does not leave AgentX broken after a node restart.&lt;/p>
&lt;hr>
&lt;h2 id="part-2--the-x-mcp-bridge-pod-xurl--supergateway">Part 2 — The X MCP bridge pod (xurl + supergateway)&lt;/h2>
&lt;p>This is the piece that turns official X MCP into something agentgateway can proxy.&lt;/p>
&lt;p>From &lt;code>config/mcp-servers/x-mcp-server.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PersistentVolumeClaim&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">accessModes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;ReadWriteOnce&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storageClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">longhorn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1Gi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># mandatory — RWO PVC + one OAuth owner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Recreate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node:20-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-c&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> npx -y supergateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --stdio &amp;#34;npx -y @xdevplatform/xurl mcp https://api.x.com/mcp&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --outputTransport streamableHttp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --stateful
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --sessionTimeout 600000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --port 8080&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HOME&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">REDIRECT_URI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://localhost:8080/callback&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_ID&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_SECRET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-app-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">CLIENT_SECRET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tcpSocket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu: 100m, memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu: 500m, memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">512Mi }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">persistentVolumeClaim&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">appProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterIP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why &lt;code>replicas: 1&lt;/code> and &lt;code>Recreate&lt;/code>? The OAuth refresh token lives on a &lt;strong>single RWO volume&lt;/strong> under &lt;code>/data&lt;/code> (&lt;code>HOME=/data&lt;/code> → &lt;code>~/.xurl&lt;/code>). Scaling the bridge would fight for the volume and corrupt the token owner story.&lt;/p>
&lt;hr>
&lt;h2 id="part-3--one-time-oauth-bootstrap-headless-inside-the-pod">Part 3 — One-time OAuth bootstrap (headless, inside the pod)&lt;/h2>
&lt;p>The bridge cannot mint the first user token without a browser once. Do this after the Deployment is Ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system &lt;span class="nb">exec&lt;/span> -it deploy/x-mcp-bridge -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> npx -y @xdevplatform/xurl auth oauth2 --headless
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>Open the printed authorization URL in a browser&lt;/li>
&lt;li>Authorize the X app for your account&lt;/li>
&lt;li>The browser redirects to &lt;code>http://localhost:8080/callback?...&lt;/code> (the page will fail to load — that&amp;rsquo;s fine)&lt;/li>
&lt;li>Paste the &lt;strong>full redirect URL&lt;/strong> back into the terminal&lt;/li>
&lt;/ol>
&lt;p>Confirm the token landed on the PVC and a live call works:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system &lt;span class="nb">exec&lt;/span> deploy/x-mcp-bridge -- ls -la /data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system &lt;span class="nb">exec&lt;/span> deploy/x-mcp-bridge -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sh -c &lt;span class="s1">&amp;#39;npx -y @xdevplatform/xurl whoami 2&amp;gt;/dev/null&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see your handle / user id. Refresh tokens rotate in place on the PVC — do &lt;strong>not&lt;/strong> delete that volume casually.&lt;/p>
&lt;hr>
&lt;h2 id="part-4--front-the-bridge-with-agentgateway">Part 4 — Front the bridge with agentgateway&lt;/h2>
&lt;h3 id="backend">Backend&lt;/h3>
&lt;p>&lt;code>config/backends/x-mcp.yaml&lt;/code> — no &lt;code>auth.secretRef&lt;/code>. Auth is already inside the bridge via xurl.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp-bridge.agentgateway-system.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StreamableHTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="route">Route&lt;/h3>
&lt;p>&lt;code>config/routes/x-mcp-route.yaml&lt;/code> attaches &lt;code>/x&lt;/code> to the &lt;strong>virtual MCP gateway&lt;/strong> (the same aggregator that serves &lt;code>/github&lt;/code>, &lt;code>/drone&lt;/code>, etc.):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">virtual-mcp-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/x&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Smoke-test through the gateway (adjust NodePort / IP for your cluster):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -sS -X POST http://&amp;lt;worker-node&amp;gt;:31606/x &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Content-Type: application/json&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s1">&amp;#39;Accept: application/json, text/event-stream&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;jsonrpc&amp;#34;:&amp;#34;2.0&amp;#34;,&amp;#34;id&amp;#34;:1,&amp;#34;method&amp;#34;:&amp;#34;tools/list&amp;#34;,&amp;#34;params&amp;#34;:{}}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should get a JSON-RPC &lt;code>tools&lt;/code> list with the X tool names. That&amp;rsquo;s the contract kagent will discover next.&lt;/p>
&lt;p>The Solo UI for agentgateway is the control plane for routes, cost, and playground wiring once backends exist:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentgateway-playground.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-11-agentx-x-mcp-agentgateway/agentgateway-playground.png" alt="Solo Enterprise for agentgateway — Playground / Select a Route empty state before MCP routes are programmed" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>After ArgoCD syncs the backend + HTTPRoute, &lt;code>/x&lt;/code> shows up on that virtual MCP gateway path — not as a bare LLM playground route, but as a first-class MCP target the rest of the platform can share.&lt;/p>
&lt;hr>
&lt;h2 id="part-5--spacexai-model-path-grok-through-the-gateway">Part 5 — SpaceXAI model path (Grok through the gateway)&lt;/h2>
&lt;p>AgentX does &lt;strong>not&lt;/strong> call &lt;code>api.x.ai&lt;/code> directly. kagent&amp;rsquo;s &lt;code>ModelConfig&lt;/code> points at the dedicated &lt;strong>xAI Grok gateway&lt;/strong> already in the cluster, with a throwaway client key (agentgateway injects the real &lt;code>XAI_API_KEY&lt;/code> upstream):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ModelConfig&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OpenAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4.5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeySecret&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKeySecretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">baseUrl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://xai-grok-gateway.agentgateway-system.svc.cluster.local/grok/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the SpaceXAI half of the story: &lt;strong>Grok-4.5 for reasoning&lt;/strong>, metered and traced by agentgateway, same as every other model in the lab.&lt;/p>
&lt;hr>
&lt;h2 id="part-6--kagent-remotemcpserver--agentx">Part 6 — kagent RemoteMCPServer + AgentX&lt;/h2>
&lt;h3 id="remotemcpserver">RemoteMCPServer&lt;/h3>
&lt;p>kagent must discover tools &lt;strong>through&lt;/strong> agentgateway, not by talking to the bridge Service directly. That keeps MCP traffic on the governed path (and consistent with GitHub / drone / FortiGate).&lt;/p>
&lt;p>&lt;code>config/kagent-models/x-remotemcpserver.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;X (Twitter) MCP via agentgateway /x — trends, news, post/user search &amp;amp; lookup, timelines, mentions, and bookmarks&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://virtual-mcp-gateway.agentgateway-system.svc.cluster.local/x&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;30s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sseReadTimeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5m0s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="agent-cr">Agent CR&lt;/h3>
&lt;p>&lt;code>config/kagent-models/agentx.yaml&lt;/code> (trimmed for readability — full file is in the repo):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AI agent for X (Twitter) — analyze trends and news, search and read posts/users, read timelines and mentions, and manage bookmarks&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">a2aConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">skills&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-research-curation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">X (Twitter) Research &amp;amp; Curation&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Analyze trends and news, search and read posts and users, read timelines and mentions, and organize bookmarks on X&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">examples&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;What&amp;#39;s trending in the US right now?&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Search recent posts about AI agents and summarize the themes&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Bookmark this post for me: &amp;lt;post-id&amp;gt;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">x, twitter, social, trends, research]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are {{.AgentName}}, an expert X research and curation assistant.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You operate on the account authenticated in the X MCP bridge.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Capabilities
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Trends, news, search, post/user lookup, timelines, mentions, bookmarks.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Limitation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> This MCP server does NOT provide posting, replying, liking, reposting,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> or media upload. Draft text instead; offer bookmark + research workflows.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Rules
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1. Read before acting — ground claims in tool output.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2. Confirm every bookmark write.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3. Never fabricate counts or handles.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 4. Cite post IDs / links.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 5. Map locations to WOEIDs (USA = 23424977, worldwide = 1).
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 6. Batch lookups to respect X rate limits.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Available tools: {{.ToolNames}}&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">x-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_trends_by_woeid&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">search_news&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_news&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">search_posts_all&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_by_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_by_ids&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_counts_recent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_liking_users&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_reposted_by&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_posts_quoted_posts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_me&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_by_username&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_by_usernames&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_by_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">search_users&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_timeline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_posts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_mentions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_bookmarks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_bookmark_folders&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">get_users_bookmarks_by_folder_id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark_folder&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_users_bookmark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_users_bookmark_folder&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two details that matter in production:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>runtime: python&lt;/code>&lt;/strong> — pins the published kagent app image. The default Go runtime image can &lt;code>ImagePullBackOff&lt;/code> depending on your registry digest story; every healthy agent in this lab uses Python.&lt;/li>
&lt;li>&lt;strong>&lt;code>requireApproval&lt;/code>&lt;/strong> on the three bookmark mutations — HITL at the tool layer, plus the system prompt that asks the model to confirm in natural language first.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="deploy-path-gitops">Deploy path (GitOps)&lt;/h2>
&lt;p>In k8s-goose, a push to &lt;code>main&lt;/code> is the deploy:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>File&lt;/th>
&lt;th>ArgoCD owner&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>config/external-secrets/x-app-external-secret.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/mcp-servers/x-mcp-server.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/backends/x-mcp.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/routes/x-mcp-route.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>agentgateway-config&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/kagent-models/x-remotemcpserver.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-models&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config/kagent-models/agentx.yaml&lt;/code>&lt;/td>
&lt;td>&lt;code>kagent-models&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Order of operations:&lt;/p>
&lt;ol>
&lt;li>Seed Vault → ExternalSecret → &lt;code>x-app-credentials&lt;/code>&lt;/li>
&lt;li>Bridge Deployment + PVC + Service&lt;/li>
&lt;li>OAuth bootstrap (manual, once)&lt;/li>
&lt;li>Backend + &lt;code>/x&lt;/code> HTTPRoute&lt;/li>
&lt;li>RemoteMCPServer + AgentX&lt;/li>
&lt;li>Chat in Solo UI&lt;/li>
&lt;/ol>
&lt;p>Verify:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n agentgateway-system get deploy,svc,pvc,agentgatewaybackend,httproute &lt;span class="p">|&lt;/span> grep x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n kagent get remotemcpserver x-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl -n kagent get agent agentx
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agent Ready, then open the kagent product surface in Solo UI, select &lt;strong>agentx&lt;/strong>, and ask:&lt;/p>
&lt;blockquote>
&lt;p>What&amp;rsquo;s trending in the US right now?&lt;/p>
&lt;/blockquote>
&lt;p>You should see a tool call to &lt;code>get_trends_by_woeid&lt;/code> (WOEID &lt;code>23424977&lt;/code>) and a clean summary — not a generic model guess.&lt;/p>
&lt;hr>
&lt;h2 id="security-and-operational-gotchas">Security and operational gotchas&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Gotcha&lt;/th>
&lt;th>Why it bites&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Scale the bridge to 2&lt;/strong>&lt;/td>
&lt;td>RWO PVC + single OAuth token&lt;/td>
&lt;td>Keep &lt;code>replicas: 1&lt;/code>, &lt;code>strategy: Recreate&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Wipe the PVC&lt;/strong>&lt;/td>
&lt;td>Refresh token is gone&lt;/td>
&lt;td>Re-run headless OAuth bootstrap&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Dev-mode Vault reboot&lt;/strong>&lt;/td>
&lt;td>&lt;code>agentgateway/x&lt;/code> disappears&lt;/td>
&lt;td>Re-run &lt;code>configure-vault.sh&lt;/code>, force-sync ExternalSecret&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Missing callback URI&lt;/strong>&lt;/td>
&lt;td>OAuth never completes&lt;/td>
&lt;td>Register &lt;code>http://localhost:8080/callback&lt;/code> on the X app&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>App-only Bearer only&lt;/strong>&lt;/td>
&lt;td>No user context / bookmarks&lt;/td>
&lt;td>Use the full xurl OAuth user token path&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secrets in chat or Git&lt;/strong>&lt;/td>
&lt;td>Rotate immediately&lt;/td>
&lt;td>Regenerate client secret in console.x.com, re-seed Vault, restart bridge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Expecting AgentX to post&lt;/strong>&lt;/td>
&lt;td>Official MCP doesn&amp;rsquo;t expose post tools&lt;/td>
&lt;td>Draft + bookmark; custom write wrapper is a planned follow-up&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Also respect X API product terms and rate limits. Batch with &lt;code>*_by_ids&lt;/code> tools when you can. This is a research/curation agent on &lt;strong>your&lt;/strong> account context — treat it like a privileged console, not a public bot.&lt;/p>
&lt;hr>
&lt;h2 id="what-this-unlocks-next">What this unlocks next&lt;/h2>
&lt;p>AgentX is deliberately the same shape as the rest of the multi-agent lab:&lt;/p>
&lt;ul>
&lt;li>Add &lt;code>@x&lt;/code> to the &lt;a href="https://maniak.io/articles/2026-07-11-telegram-multi-agent-bot-kagent-agentgateway/">Telegram multi-agent bot&lt;/a> via A2A&lt;/li>
&lt;li>Point a scheduled job at morning trend digests (agent-scheduler → PR or chat)&lt;/li>
&lt;li>Layer enterprise tool modes (Search / Code / CodeSearch) if the X tool catalog grows large enough that Standard mode burns context&lt;/li>
&lt;li>Build a &lt;strong>write&lt;/strong> MCP wrapper on top of xurl CLI for posting — behind HITL, never free-fire&lt;/li>
&lt;/ul>
&lt;p>The point of agentgateway in front of MCP is not ceremony. It&amp;rsquo;s that &lt;strong>every tool call is a first-class route&lt;/strong>: observable, shareable across agents, and isolated from the LLM key path. SpaceXAI handles the intelligence. X MCP handles the media surface. kagent turns both into something you can actually operate.&lt;/p>
&lt;hr>
&lt;h2 id="repo-map">Repo map&lt;/h2>
&lt;p>All manifests live here:&lt;/p>
&lt;ul>
&lt;li>Bridge: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/mcp-servers/x-mcp-server.yaml">&lt;code>config/mcp-servers/x-mcp-server.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>Backend: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/backends/x-mcp.yaml">&lt;code>config/backends/x-mcp.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>Route: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/routes/x-mcp-route.yaml">&lt;code>config/routes/x-mcp-route.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>RemoteMCPServer: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/kagent-models/x-remotemcpserver.yaml">&lt;code>config/kagent-models/x-remotemcpserver.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>AgentX: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/kagent-models/agentx.yaml">&lt;code>config/kagent-models/agentx.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;li>ExternalSecret: &lt;a href="https://github.com/sebbycorp/k8s-goose/blob/main/config/external-secrets/x-app-external-secret.yaml">&lt;code>config/external-secrets/x-app-external-secret.yaml&lt;/code>&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Platform overview: &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Interactive docs: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="wrap-up">Wrap-up&lt;/h2>
&lt;p>You now have a repeatable pattern for a personal &lt;strong>X research agent&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>SpaceXAI Grok-4.5&lt;/strong> via agentgateway (&lt;code>xai-grok&lt;/code> ModelConfig)&lt;/li>
&lt;li>&lt;strong>Official X MCP&lt;/strong> wrapped by supergateway for Streamable HTTP&lt;/li>
&lt;li>&lt;strong>agentgateway&lt;/strong> &lt;code>/x&lt;/code> on the virtual MCP gateway&lt;/li>
&lt;li>&lt;strong>kagent AgentX&lt;/strong> with a curated tool list and HITL on bookmark writes&lt;/li>
&lt;li>&lt;strong>Vault-backed&lt;/strong> app credentials and a durable OAuth token on PVC&lt;/li>
&lt;/ol>
&lt;p>Ask it what&amp;rsquo;s trending. Ask it to research a handle. Ask it to build a bookmark folder around a topic. Keep the write surface narrow until you&amp;rsquo;re ready for a custom poster.&lt;/p>
&lt;p>That&amp;rsquo;s how you stop doomscrolling the firehose — and start running an agent that actually manages the world of news, media, and posts for you.&lt;/p></content:encoded></item><item><title>I Put My Whole Homelab in a Telegram Group Chat — kagent Flies It, agentgateway Governs It</title><link>https://maniak.io/articles/2026-07-11-telegram-multi-agent-bot-kagent-agentgateway/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-11-telegram-multi-agent-bot-kagent-agentgateway/</guid><description>&lt;p>I have a group chat with my infrastructure.&lt;/p>
&lt;p>Not a metaphor. I open Telegram, type &lt;code>/use forti what devices are on my wifi?&lt;/code>, and a few seconds later my FortiGate firewall answers — in a chat bubble, on my phone, from the couch. Type &lt;code>/use k8s scale the drone-mcp deployment to 2&lt;/code> and my Kubernetes cluster does it, but first it stops and shows me an &lt;strong>Approve / Reject&lt;/strong> button, because that one writes. Type &lt;code>/use drone take off, flip, photograph the room, land&lt;/code> and an actual quadcopter in my office leaves the ground.&lt;/p>
&lt;p>Six agents. One bot. One phone. It&amp;rsquo;s the most fun I&amp;rsquo;ve had with a cluster in a while — and underneath the fun there&amp;rsquo;s a design I actually care about: &lt;strong>the bot holds no brains and no keys.&lt;/strong> Every agent lives in the cluster, every model call goes through &lt;em>my&lt;/em> &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>, and anything dangerous waits for my thumb.&lt;/p>
&lt;p>Live map: &lt;strong>&lt;a href="https://goose.maniak.ai/dispatch.html">goose.maniak.ai/dispatch.html&lt;/a>&lt;/strong> · Repo: &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Bot: &lt;strong>&lt;a href="https://t.me/KagentCorpAIbot">@KagentCorpAIbot&lt;/a>&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s the whole thing on my phone — &lt;code>/help&lt;/code> listing the six agents, and just above it the F5 agent answering &lt;em>&amp;ldquo;what are my vips?&amp;rdquo;&lt;/em> with &lt;strong>19 VIPs found, all administratively enabled&lt;/strong>:&lt;/p>
&lt;div style="max-width:360px;margin:1.5rem auto;">
&lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/bot-help-f5-vips.png" alt="Telegram chat with @KagentCorpAIbot: the /help output lists agents demo, drone, f5, forti, github, k8s with 'Current: f5' and the /start /agents /use /new /status commands; above it the f5 agent replies to a VIP query with 'Summary: 19 VIPs found, all are administratively enabled'." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;/div>
&lt;hr>
&lt;h2 id="the-cast">The cast&lt;/h2>
&lt;p>Meet &lt;code>@KagentCorpAIbot&lt;/code>. It&amp;rsquo;s a single polling Deployment in the &lt;code>kagent&lt;/code> namespace that speaks to six in-cluster agents. Each one is a real &lt;a href="https://kagent.dev">kagent&lt;/a> Agent with its own tools, its own MCP servers, and its own blast radius:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Alias&lt;/th>
&lt;th>Agent&lt;/th>
&lt;th>What it touches&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>@k8s&lt;/code>&lt;/td>
&lt;td>&lt;code>k8s-agent&lt;/code> (KubeAssist)&lt;/td>
&lt;td>The cluster itself — get/describe/logs/events, plus scale/rollout/patch/apply/&lt;strong>delete&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@forti&lt;/code>&lt;/td>
&lt;td>&lt;code>fortigate-agent&lt;/code>&lt;/td>
&lt;td>FortiGate &lt;code>172.16.10.1&lt;/code> — policies, NAT/VIPs, DHCP leases, detected devices, FortiAP wireless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@f5&lt;/code>&lt;/td>
&lt;td>&lt;code>f5-bigip-agent&lt;/code>&lt;/td>
&lt;td>F5 BIG-IP &lt;code>172.16.10.10&lt;/code> — pools, virtual servers, nodes, monitors, iRules, HA failover&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@github&lt;/code>&lt;/td>
&lt;td>&lt;code>github-agent&lt;/code>&lt;/td>
&lt;td>The remote GitHub MCP (47 tools) — issues, PRs, repo ops&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@drone&lt;/code>&lt;/td>
&lt;td>&lt;code>drone-agent&lt;/code>&lt;/td>
&lt;td>A real Ryze RoboMaster TT — 28 flight tools over an MCP server&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@demo&lt;/code>&lt;/td>
&lt;td>&lt;code>demo-agent&lt;/code>&lt;/td>
&lt;td>The sandbox MCP servers, for when I&amp;rsquo;m just poking&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s the whole point of the group chat: &lt;strong>the same chat window is a firewall console, a &lt;code>kubectl&lt;/code> prompt, a GitHub client, and a drone remote&lt;/strong> — I just switch who I&amp;rsquo;m talking to.&lt;/p>
&lt;p>Here&amp;rsquo;s &lt;code>@forti&lt;/code> doing exactly that — I asked &lt;em>&amp;ldquo;give me a list of wifi devices&amp;rdquo;&lt;/em> and the FortiGate agent came back with &lt;strong>40 devices on SSID ManiakHQ&lt;/strong>, formatted as a table right in the chat (hostname, IP, MAC, AP, band, signal, OS, vendor):&lt;/p>
&lt;div style="max-width:360px;margin:1.5rem auto;">
&lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/bot-forti-wifi-devices.png" alt="Telegram: after '/use forti give me a list of wifi devices', the FortiGate agent replies '40 total, all on SSID ManiakHQ' followed by a markdown table of devices — Office-3 Apple tvOS, an iPhone, Master-Bedroom Apple TV, a Sonos, an HP printer, a Vizio cast TV — each with IP, MAC, access point, 802.11 band, signal, OS, and vendor." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;/div>
&lt;p>That&amp;rsquo;s a real FortiGate at &lt;code>172.16.10.1&lt;/code> answering a plain-English question from my phone. No console, no SSH — just a chat bubble.&lt;/p>
&lt;hr>
&lt;h2 id="act-1--how-a-text-message-flies-a-drone">Act 1 — How a text message flies a drone&lt;/h2>
&lt;p>Here&amp;rsquo;s the loop, start to finish, when I send one message:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl"> &lt;span class="err">📱&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">ns&lt;/span> &lt;span class="n">kagent&lt;/span> &lt;span class="n">ns&lt;/span> &lt;span class="n">agentgateway&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">system&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────┐&lt;/span> &lt;span class="n">long&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">poll&lt;/span> &lt;span class="err">┌──────────────┐&lt;/span> &lt;span class="n">A2A&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="n">OpenAI&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compat&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">you&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">───────────▶&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">─────────▶&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">drone&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">──────────────▶&lt;/span> &lt;span class="n">AgentGateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="err">@&lt;/span>&lt;span class="n">drone&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="n">replica&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">kagent&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">openai&lt;/span> &lt;span class="err">·&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">grok&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="s2">&amp;#34;flip&amp;#34;&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">◀───────────&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">send&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">ModelConfig&lt;/span> &lt;span class="err">────┼──▶&lt;/span> &lt;span class="n">gateway&lt;/span> &lt;span class="err">──▶&lt;/span> &lt;span class="n">real&lt;/span> &lt;span class="n">provider&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──────────┘&lt;/span> &lt;span class="n">chat&lt;/span> &lt;span class="n">reply&lt;/span> &lt;span class="err">└──────────────┘&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span> &lt;span class="n">keys&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="n">Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">drone&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">mcp&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">server&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="err">🚁&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>&lt;strong>You message.&lt;/strong> The bot long-polls Telegram (&lt;code>getUpdates&lt;/code>) — its BotFather token comes from Vault via an ExternalSecret, never from Git. Single replica, on purpose: two pollers means a &lt;code>409 Conflict&lt;/code> fight over the same update stream.&lt;/li>
&lt;li>&lt;strong>A2A send.&lt;/strong> The bot is a thin router. It looks up which agent you&amp;rsquo;ve selected, and fires a JSON-RPC &lt;code>message/send&lt;/code> at that agent&amp;rsquo;s in-cluster Service — &lt;code>http://drone-agent.kagent.svc.cluster.local:8080/&lt;/code> — carrying a per-chat &lt;code>contextId&lt;/code> so the conversation has memory.&lt;/li>
&lt;li>&lt;strong>The agent thinks.&lt;/strong> &lt;code>drone-agent&lt;/code> runs in the kagent runtime. To reason, it calls a model — but its kagent &lt;code>ModelConfig&lt;/code> doesn&amp;rsquo;t point at OpenAI. It points at an &lt;strong>in-cluster OpenAI-compatible base URL that is the gateway.&lt;/strong>&lt;/li>
&lt;li>&lt;strong>The gateway governs.&lt;/strong> agentgateway injects the real provider key (from a Vault-synced Secret), meters the tokens, emits a trace, and &lt;em>then&lt;/em> forwards upstream. The agent never sees a credential.&lt;/li>
&lt;li>&lt;strong>The reply comes home.&lt;/strong> The answer streams back over A2A, the bot posts it as a chat bubble, and — if the agent wanted to run a tool that writes — you get a button instead of a fait accompli. (More on that in Act 2.)&lt;/li>
&lt;/ol>
&lt;p>The bot&amp;rsquo;s routing table is literally one environment variable on the Deployment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AGENTS_JSON&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;k8s&amp;#34;: &amp;#34;http://k8s-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;forti&amp;#34;: &amp;#34;http://fortigate-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;f5&amp;#34;: &amp;#34;http://f5-bigip-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;github&amp;#34;: &amp;#34;http://github-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;drone&amp;#34;: &amp;#34;http://drone-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;demo&amp;#34;: &amp;#34;http://demo-agent.kagent.svc.cluster.local:8080/&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Want a new agent in the chat? Add a line, redeploy the bot. No new brains to train, no keys to hand out — the agent already exists in the fleet, and the gateway already knows how to route its model.&lt;/p>
&lt;p>&lt;code>/status&lt;/code> pings whichever agent I&amp;rsquo;ve got selected, and &lt;code>/agents&lt;/code> prints that routing table live — the same in-cluster A2A URLs, straight from the bot:&lt;/p>
&lt;div style="max-width:360px;margin:1.5rem auto;">
&lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/bot-status-agents.png" alt="Telegram: '/status' returns 'f5 reachable http://f5-bigip-agent.kagent.svc.cluster.local:8080/ HTTP 200', then '/agents' lists all six aliases mapped to their in-cluster A2A Service URLs — demo, drone, f5 (marked current), forti, github, k8s — each at kagent.svc.cluster.local:8080." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;/div>
&lt;p>The commands are deliberately boring so I can drive them one-handed:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Command&lt;/th>
&lt;th>Action&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/start&lt;/code>&lt;/td>
&lt;td>Help&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/agents&lt;/code>&lt;/td>
&lt;td>List aliases + their A2A URLs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/use &amp;lt;alias&amp;gt; [msg]&lt;/code>&lt;/td>
&lt;td>Switch agent — and optionally ask in the same line&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/new&lt;/code>&lt;/td>
&lt;td>Reset the session (fresh &lt;code>contextId&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/status&lt;/code>&lt;/td>
&lt;td>Ping the current agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@forti …&lt;/code>&lt;/td>
&lt;td>Switch &lt;em>and&lt;/em> message in one shot&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>/use f5 what are my vips?&lt;/code> is a complete interaction: pick the F5 agent and ask it, in one thumb-stroke.&lt;/p>
&lt;hr>
&lt;h2 id="act-2--the-thumb-between-me-and-a-delete">Act 2 — The thumb between me and a &lt;code>delete&lt;/code>&lt;/h2>
&lt;p>Here&amp;rsquo;s the part I refuse to skip. &lt;code>@k8s&lt;/code> can delete deployments. &lt;code>@forti&lt;/code> can rewrite firewall policy. &lt;code>@f5&lt;/code> can drain a pool member. A chatbot with that reach is a great way to nuke prod from a bus stop.&lt;/p>
&lt;p>So the destructive tools are wrapped in kagent&amp;rsquo;s &lt;strong>human-in-the-loop&lt;/strong> approval (&lt;code>requireApproval&lt;/code>), and the bot surfaces it natively. When an agent decides it wants to run a write, it doesn&amp;rsquo;t run it — it returns an &lt;code>adk_request_confirmation&lt;/code> back over A2A. The bot parses that, shows me exactly what&amp;rsquo;s about to happen, and hangs two inline buttons under it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">The agent wants to run: apply_manifest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> namespace: kagent
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apiVersion: apps/v1
kind: Deployment
&amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> [ ✅ Approve ] [ ❌ Reject ]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tap &lt;strong>Approve&lt;/strong> and the bot sends the decision back — the agent resumes and completes the tool call. Tap &lt;strong>Reject&lt;/strong> and nothing happens. The read paths (&lt;code>get&lt;/code>, &lt;code>describe&lt;/code>, &lt;code>logs&lt;/code>, &amp;ldquo;what devices are on my wifi&amp;rdquo;) flow straight through; only the writes stop. It&amp;rsquo;s the difference between &lt;em>&amp;ldquo;the bot did a thing&amp;rdquo;&lt;/em> and &lt;em>&amp;ldquo;I did a thing, from my phone, with a receipt.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>And the receipt is real: because every model hop went through agentgateway, each of these interactions is a &lt;strong>priced, traced span in Langfuse&lt;/strong> and a row in the Solo cost UI — split by project, by model, by agent. I can see what my group chat cost me this week.&lt;/p>
&lt;hr>
&lt;h2 id="act-3--the-keys-never-leave-home">Act 3 — The keys never leave home&lt;/h2>
&lt;p>The whole thing rests on one rule: &lt;strong>no agent, and no channel, ever holds a provider API key.&lt;/strong>&lt;/p>
&lt;p>The fleet the bot talks to is just the kagent Agents list in the Solo Enterprise UI — every agent &lt;strong>Healthy&lt;/strong>, each with its &lt;code>Model&lt;/code> column showing what it routes to (OpenAI &lt;code>gpt-5.5&lt;/code>, local &lt;code>Qwen&lt;/code>) and its required MCP tool servers (&lt;code>drone-mcp&lt;/code>, &lt;code>f5-bigip-…&lt;/code>):&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/kagent-agents-fleet.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/kagent-agents-fleet.png" alt="Solo Enterprise for kagent — the Agents list, every agent Healthy on cluster mgmt-cluster / namespace kagent, with Model column showing OpenAI (gpt-5.5) and OpenAI (Qwen) and Required Tools listing drone-mcp and f5-bigip servers." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>That &lt;code>Model&lt;/code> column is the sleight of hand: it says &amp;ldquo;OpenAI (gpt-5.5)&amp;rdquo; and &amp;ldquo;OpenAI (Qwen),&amp;rdquo; but both are OpenAI-&lt;em>compatible&lt;/em> endpoints that resolve to my gateway — not to OpenAI.&lt;/p>
&lt;p>kagent &lt;code>ModelConfig&lt;/code>s point at in-cluster gateways, not at providers:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>ModelConfig&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Gateway base URL (in-cluster)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>default-model-config&lt;/code>&lt;/td>
&lt;td>lab default&lt;/td>
&lt;td>&lt;code>…/openai/v1&lt;/code> · &lt;code>agentgateway-proxy&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>xai-grok&lt;/code>&lt;/td>
&lt;td>&lt;code>grok-4.5&lt;/code>&lt;/td>
&lt;td>&lt;code>…/grok/v1&lt;/code> · &lt;code>xai-grok-gateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dgx-spark&lt;/code>&lt;/td>
&lt;td>Qwen (local, self-hosted)&lt;/td>
&lt;td>&lt;code>…/spark/v1&lt;/code> · &lt;code>dgx-spark-gateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gpt-5-6&lt;/code>&lt;/td>
&lt;td>&lt;code>gpt-5.6&lt;/code>&lt;/td>
&lt;td>&lt;code>…/gpt56/v1&lt;/code> · dedicated gateway&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The real OpenAI, xAI, and Anthropic keys sit in Vault. External Secrets Operator syncs them into gateway-side Secrets. agentgateway injects them upstream at call time. Which means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>One control plane for spend, traces, and budgets&lt;/strong> — not one per app.&lt;/li>
&lt;li>&lt;strong>Agents are credential-free.&lt;/strong> Compromise a pod, you get no keys.&lt;/li>
&lt;li>&lt;strong>I can swap &lt;code>gpt-5.5&lt;/code> for Qwen-on-my-DGX-Spark&lt;/strong> without touching the bot, the scheduler, or a single agent. It&amp;rsquo;s a one-line &lt;code>ModelConfig&lt;/code> change.&lt;/li>
&lt;li>&lt;strong>Cost and Langfuse stay accurate across channels&lt;/strong> — chat and cron land in the same ledger.&lt;/li>
&lt;/ul>
&lt;p>That last point matters more than it sounds, because the chat bot isn&amp;rsquo;t the only way in.&lt;/p>
&lt;hr>
&lt;h2 id="act-4--the-cron-twin">Act 4 — The cron twin&lt;/h2>
&lt;p>&lt;code>@KagentCorpAIbot&lt;/code> is for &lt;em>&amp;ldquo;help me right now.&amp;rdquo;&lt;/em> But some things should just happen every morning — a cluster health sweep, a firewall device audit, a &amp;ldquo;what PRs are stale&amp;rdquo; report. For that there&amp;rsquo;s a second front-end on the exact same fleet: the &lt;strong>Agent Scheduler&lt;/strong>, and it is pure GitOps.&lt;/p>
&lt;p>Here&amp;rsquo;s the twist I like: the scheduler&amp;rsquo;s web UI &lt;strong>never creates a CronJob through the Kubernetes API.&lt;/strong> It can&amp;rsquo;t set desired state directly. All it does is open a &lt;strong>pull request&lt;/strong> against &lt;code>sebbycorp/k8s-goose&lt;/code>. ArgoCD notices, merges, and applies. Git stays the one source of truth; the cluster only ever &lt;em>executes&lt;/em> what Git says.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> Web UI (:30955) GitHub ArgoCD ns kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌─────────────┐ PR + ┌──────────────┐ sync ┌────────────┐ apply ┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ save a │ ──────▶ │ config/agent-│ ─────▶ │ agentgw- │ ──────▶ │ CronJob │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ schedule │ merge │ schedules/* │ │ config app │ │ ags-&amp;lt;name&amp;gt; │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────────┘ └──────────────┘ └────────────┘ └──────┬───────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ fires
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Job → A2A message/send → agent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> result → ConfigMap ags-result-*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (± Telegram summary, same bot token)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When a scheduled Job fires, it POSTs the same A2A &lt;code>message/send&lt;/code> to the same agent Service the chat bot uses, and writes the reply into ConfigMaps (&lt;code>ags-result-&amp;lt;name&amp;gt;&lt;/code> for the latest, &lt;code>ags-hist-…&lt;/code> for history). Check the box marked &lt;em>&amp;ldquo;send result to Telegram&amp;rdquo;&lt;/em> and the Job posts a summary to my chat using the &lt;strong>same Vault bot token&lt;/strong> — so my morning cluster report shows up in the same window where I do my ad-hoc asks.&lt;/p>
&lt;p>One caveat worth stating plainly: that beautiful HITL approval gate is a &lt;em>problem&lt;/em> for unattended cron. A Job can&amp;rsquo;t tap Approve. So scheduled prompts start read-only — audits, reports, &amp;ldquo;tell me what changed&amp;rdquo; — and the writes stay in the interactive channel where a human thumb is present.&lt;/p>
&lt;hr>
&lt;h2 id="the-shape-of-it">The shape of it&lt;/h2>
&lt;p>Strip away the fun and here&amp;rsquo;s what&amp;rsquo;s actually true:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Two front-ends, one fleet.&lt;/strong> A Telegram bot for conversation, a GitOps scheduler for routines — both are thin edge adapters that speak A2A to the &lt;em>same&lt;/em> in-cluster kagent agents.&lt;/li>
&lt;li>&lt;strong>kagent runs the brains.&lt;/strong> Six agents, each with its own MCP tool servers and its own RBAC/blast-radius, reachable at &lt;code>:8080&lt;/code> over A2A.&lt;/li>
&lt;li>&lt;strong>agentgateway is the governor.&lt;/strong> Every model call is priced, traced, key-injected, and policy-checked before it reaches a provider. Keys live in Vault; nothing sensitive is in Git.&lt;/li>
&lt;li>&lt;strong>My thumb is the last mile.&lt;/strong> Destructive tools stop for an inline Approve/Reject.&lt;/li>
&lt;/ul>
&lt;p>It started as a joke — &lt;em>&amp;ldquo;what if I could text my firewall?&amp;rdquo;&lt;/em> — and turned into the cleanest way I&amp;rsquo;ve found to operate a homelab: fun on the surface, governed all the way down.&lt;/p>
&lt;p>If you want to build your own, the whole thing is GitOps&amp;rsquo;d in &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> — bot source in &lt;code>telegram-bot/&lt;/code>, scheduler in &lt;code>agent-scheduler/&lt;/code>, and the operator runbook (seed the Vault bot token, verify the ExternalSecret, smoke-test the chat path) on the live &lt;strong>&lt;a href="https://goose.maniak.ai/dispatch.html">Dispatch page&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>Now if you&amp;rsquo;ll excuse me, I have a drone to fly from the kitchen.&lt;/p></description><content:encoded>&lt;p>I have a group chat with my infrastructure.&lt;/p>
&lt;p>Not a metaphor. I open Telegram, type &lt;code>/use forti what devices are on my wifi?&lt;/code>, and a few seconds later my FortiGate firewall answers — in a chat bubble, on my phone, from the couch. Type &lt;code>/use k8s scale the drone-mcp deployment to 2&lt;/code> and my Kubernetes cluster does it, but first it stops and shows me an &lt;strong>Approve / Reject&lt;/strong> button, because that one writes. Type &lt;code>/use drone take off, flip, photograph the room, land&lt;/code> and an actual quadcopter in my office leaves the ground.&lt;/p>
&lt;p>Six agents. One bot. One phone. It&amp;rsquo;s the most fun I&amp;rsquo;ve had with a cluster in a while — and underneath the fun there&amp;rsquo;s a design I actually care about: &lt;strong>the bot holds no brains and no keys.&lt;/strong> Every agent lives in the cluster, every model call goes through &lt;em>my&lt;/em> &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>, and anything dangerous waits for my thumb.&lt;/p>
&lt;p>Live map: &lt;strong>&lt;a href="https://goose.maniak.ai/dispatch.html">goose.maniak.ai/dispatch.html&lt;/a>&lt;/strong> · Repo: &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> · Bot: &lt;strong>&lt;a href="https://t.me/KagentCorpAIbot">@KagentCorpAIbot&lt;/a>&lt;/strong>&lt;/p>
&lt;p>Here&amp;rsquo;s the whole thing on my phone — &lt;code>/help&lt;/code> listing the six agents, and just above it the F5 agent answering &lt;em>&amp;ldquo;what are my vips?&amp;rdquo;&lt;/em> with &lt;strong>19 VIPs found, all administratively enabled&lt;/strong>:&lt;/p>
&lt;div style="max-width:360px;margin:1.5rem auto;">
&lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/bot-help-f5-vips.png" alt="Telegram chat with @KagentCorpAIbot: the /help output lists agents demo, drone, f5, forti, github, k8s with 'Current: f5' and the /start /agents /use /new /status commands; above it the f5 agent replies to a VIP query with 'Summary: 19 VIPs found, all are administratively enabled'." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;/div>
&lt;hr>
&lt;h2 id="the-cast">The cast&lt;/h2>
&lt;p>Meet &lt;code>@KagentCorpAIbot&lt;/code>. It&amp;rsquo;s a single polling Deployment in the &lt;code>kagent&lt;/code> namespace that speaks to six in-cluster agents. Each one is a real &lt;a href="https://kagent.dev">kagent&lt;/a> Agent with its own tools, its own MCP servers, and its own blast radius:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Alias&lt;/th>
&lt;th>Agent&lt;/th>
&lt;th>What it touches&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>@k8s&lt;/code>&lt;/td>
&lt;td>&lt;code>k8s-agent&lt;/code> (KubeAssist)&lt;/td>
&lt;td>The cluster itself — get/describe/logs/events, plus scale/rollout/patch/apply/&lt;strong>delete&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@forti&lt;/code>&lt;/td>
&lt;td>&lt;code>fortigate-agent&lt;/code>&lt;/td>
&lt;td>FortiGate &lt;code>172.16.10.1&lt;/code> — policies, NAT/VIPs, DHCP leases, detected devices, FortiAP wireless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@f5&lt;/code>&lt;/td>
&lt;td>&lt;code>f5-bigip-agent&lt;/code>&lt;/td>
&lt;td>F5 BIG-IP &lt;code>172.16.10.10&lt;/code> — pools, virtual servers, nodes, monitors, iRules, HA failover&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@github&lt;/code>&lt;/td>
&lt;td>&lt;code>github-agent&lt;/code>&lt;/td>
&lt;td>The remote GitHub MCP (47 tools) — issues, PRs, repo ops&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@drone&lt;/code>&lt;/td>
&lt;td>&lt;code>drone-agent&lt;/code>&lt;/td>
&lt;td>A real Ryze RoboMaster TT — 28 flight tools over an MCP server&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@demo&lt;/code>&lt;/td>
&lt;td>&lt;code>demo-agent&lt;/code>&lt;/td>
&lt;td>The sandbox MCP servers, for when I&amp;rsquo;m just poking&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That&amp;rsquo;s the whole point of the group chat: &lt;strong>the same chat window is a firewall console, a &lt;code>kubectl&lt;/code> prompt, a GitHub client, and a drone remote&lt;/strong> — I just switch who I&amp;rsquo;m talking to.&lt;/p>
&lt;p>Here&amp;rsquo;s &lt;code>@forti&lt;/code> doing exactly that — I asked &lt;em>&amp;ldquo;give me a list of wifi devices&amp;rdquo;&lt;/em> and the FortiGate agent came back with &lt;strong>40 devices on SSID ManiakHQ&lt;/strong>, formatted as a table right in the chat (hostname, IP, MAC, AP, band, signal, OS, vendor):&lt;/p>
&lt;div style="max-width:360px;margin:1.5rem auto;">
&lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/bot-forti-wifi-devices.png" alt="Telegram: after '/use forti give me a list of wifi devices', the FortiGate agent replies '40 total, all on SSID ManiakHQ' followed by a markdown table of devices — Office-3 Apple tvOS, an iPhone, Master-Bedroom Apple TV, a Sonos, an HP printer, a Vizio cast TV — each with IP, MAC, access point, 802.11 band, signal, OS, and vendor." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;/div>
&lt;p>That&amp;rsquo;s a real FortiGate at &lt;code>172.16.10.1&lt;/code> answering a plain-English question from my phone. No console, no SSH — just a chat bubble.&lt;/p>
&lt;hr>
&lt;h2 id="act-1--how-a-text-message-flies-a-drone">Act 1 — How a text message flies a drone&lt;/h2>
&lt;p>Here&amp;rsquo;s the loop, start to finish, when I send one message:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl"> &lt;span class="err">📱&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">ns&lt;/span> &lt;span class="n">kagent&lt;/span> &lt;span class="n">ns&lt;/span> &lt;span class="n">agentgateway&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">system&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────┐&lt;/span> &lt;span class="n">long&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">poll&lt;/span> &lt;span class="err">┌──────────────┐&lt;/span> &lt;span class="n">A2A&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="n">OpenAI&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">compat&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">you&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">───────────▶&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">─────────▶&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">drone&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">──────────────▶&lt;/span> &lt;span class="n">AgentGateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="err">@&lt;/span>&lt;span class="n">drone&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span> &lt;span class="n">replica&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">kagent&lt;/span> &lt;span class="n">runtime&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">openai&lt;/span> &lt;span class="err">·&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">grok&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="s2">&amp;#34;flip&amp;#34;&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">◀───────────&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">send&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">ModelConfig&lt;/span> &lt;span class="err">────┼──▶&lt;/span> &lt;span class="n">gateway&lt;/span> &lt;span class="err">──▶&lt;/span> &lt;span class="n">real&lt;/span> &lt;span class="n">provider&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──────────┘&lt;/span> &lt;span class="n">chat&lt;/span> &lt;span class="n">reply&lt;/span> &lt;span class="err">└──────────────┘&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span> &lt;span class="n">keys&lt;/span> &lt;span class="n">from&lt;/span> &lt;span class="n">Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">drone&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">mcp&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">server&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="err">🚁&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>&lt;strong>You message.&lt;/strong> The bot long-polls Telegram (&lt;code>getUpdates&lt;/code>) — its BotFather token comes from Vault via an ExternalSecret, never from Git. Single replica, on purpose: two pollers means a &lt;code>409 Conflict&lt;/code> fight over the same update stream.&lt;/li>
&lt;li>&lt;strong>A2A send.&lt;/strong> The bot is a thin router. It looks up which agent you&amp;rsquo;ve selected, and fires a JSON-RPC &lt;code>message/send&lt;/code> at that agent&amp;rsquo;s in-cluster Service — &lt;code>http://drone-agent.kagent.svc.cluster.local:8080/&lt;/code> — carrying a per-chat &lt;code>contextId&lt;/code> so the conversation has memory.&lt;/li>
&lt;li>&lt;strong>The agent thinks.&lt;/strong> &lt;code>drone-agent&lt;/code> runs in the kagent runtime. To reason, it calls a model — but its kagent &lt;code>ModelConfig&lt;/code> doesn&amp;rsquo;t point at OpenAI. It points at an &lt;strong>in-cluster OpenAI-compatible base URL that is the gateway.&lt;/strong>&lt;/li>
&lt;li>&lt;strong>The gateway governs.&lt;/strong> agentgateway injects the real provider key (from a Vault-synced Secret), meters the tokens, emits a trace, and &lt;em>then&lt;/em> forwards upstream. The agent never sees a credential.&lt;/li>
&lt;li>&lt;strong>The reply comes home.&lt;/strong> The answer streams back over A2A, the bot posts it as a chat bubble, and — if the agent wanted to run a tool that writes — you get a button instead of a fait accompli. (More on that in Act 2.)&lt;/li>
&lt;/ol>
&lt;p>The bot&amp;rsquo;s routing table is literally one environment variable on the Deployment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AGENTS_JSON&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;k8s&amp;#34;: &amp;#34;http://k8s-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;forti&amp;#34;: &amp;#34;http://fortigate-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;f5&amp;#34;: &amp;#34;http://f5-bigip-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;github&amp;#34;: &amp;#34;http://github-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;drone&amp;#34;: &amp;#34;http://drone-agent.kagent.svc.cluster.local:8080/&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;demo&amp;#34;: &amp;#34;http://demo-agent.kagent.svc.cluster.local:8080/&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Want a new agent in the chat? Add a line, redeploy the bot. No new brains to train, no keys to hand out — the agent already exists in the fleet, and the gateway already knows how to route its model.&lt;/p>
&lt;p>&lt;code>/status&lt;/code> pings whichever agent I&amp;rsquo;ve got selected, and &lt;code>/agents&lt;/code> prints that routing table live — the same in-cluster A2A URLs, straight from the bot:&lt;/p>
&lt;div style="max-width:360px;margin:1.5rem auto;">
&lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/bot-status-agents.png" alt="Telegram: '/status' returns 'f5 reachable http://f5-bigip-agent.kagent.svc.cluster.local:8080/ HTTP 200', then '/agents' lists all six aliases mapped to their in-cluster A2A Service URLs — demo, drone, f5 (marked current), forti, github, k8s — each at kagent.svc.cluster.local:8080." style="width:100%;height:auto;border-radius:14px;border:1px solid rgba(255,255,255,.08);" />
&lt;/div>
&lt;p>The commands are deliberately boring so I can drive them one-handed:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Command&lt;/th>
&lt;th>Action&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/start&lt;/code>&lt;/td>
&lt;td>Help&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/agents&lt;/code>&lt;/td>
&lt;td>List aliases + their A2A URLs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/use &amp;lt;alias&amp;gt; [msg]&lt;/code>&lt;/td>
&lt;td>Switch agent — and optionally ask in the same line&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/new&lt;/code>&lt;/td>
&lt;td>Reset the session (fresh &lt;code>contextId&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/status&lt;/code>&lt;/td>
&lt;td>Ping the current agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@forti …&lt;/code>&lt;/td>
&lt;td>Switch &lt;em>and&lt;/em> message in one shot&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>/use f5 what are my vips?&lt;/code> is a complete interaction: pick the F5 agent and ask it, in one thumb-stroke.&lt;/p>
&lt;hr>
&lt;h2 id="act-2--the-thumb-between-me-and-a-delete">Act 2 — The thumb between me and a &lt;code>delete&lt;/code>&lt;/h2>
&lt;p>Here&amp;rsquo;s the part I refuse to skip. &lt;code>@k8s&lt;/code> can delete deployments. &lt;code>@forti&lt;/code> can rewrite firewall policy. &lt;code>@f5&lt;/code> can drain a pool member. A chatbot with that reach is a great way to nuke prod from a bus stop.&lt;/p>
&lt;p>So the destructive tools are wrapped in kagent&amp;rsquo;s &lt;strong>human-in-the-loop&lt;/strong> approval (&lt;code>requireApproval&lt;/code>), and the bot surfaces it natively. When an agent decides it wants to run a write, it doesn&amp;rsquo;t run it — it returns an &lt;code>adk_request_confirmation&lt;/code> back over A2A. The bot parses that, shows me exactly what&amp;rsquo;s about to happen, and hangs two inline buttons under it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">The agent wants to run: apply_manifest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> namespace: kagent
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apiVersion: apps/v1
kind: Deployment
&amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> [ ✅ Approve ] [ ❌ Reject ]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tap &lt;strong>Approve&lt;/strong> and the bot sends the decision back — the agent resumes and completes the tool call. Tap &lt;strong>Reject&lt;/strong> and nothing happens. The read paths (&lt;code>get&lt;/code>, &lt;code>describe&lt;/code>, &lt;code>logs&lt;/code>, &amp;ldquo;what devices are on my wifi&amp;rdquo;) flow straight through; only the writes stop. It&amp;rsquo;s the difference between &lt;em>&amp;ldquo;the bot did a thing&amp;rdquo;&lt;/em> and &lt;em>&amp;ldquo;I did a thing, from my phone, with a receipt.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>And the receipt is real: because every model hop went through agentgateway, each of these interactions is a &lt;strong>priced, traced span in Langfuse&lt;/strong> and a row in the Solo cost UI — split by project, by model, by agent. I can see what my group chat cost me this week.&lt;/p>
&lt;hr>
&lt;h2 id="act-3--the-keys-never-leave-home">Act 3 — The keys never leave home&lt;/h2>
&lt;p>The whole thing rests on one rule: &lt;strong>no agent, and no channel, ever holds a provider API key.&lt;/strong>&lt;/p>
&lt;p>The fleet the bot talks to is just the kagent Agents list in the Solo Enterprise UI — every agent &lt;strong>Healthy&lt;/strong>, each with its &lt;code>Model&lt;/code> column showing what it routes to (OpenAI &lt;code>gpt-5.5&lt;/code>, local &lt;code>Qwen&lt;/code>) and its required MCP tool servers (&lt;code>drone-mcp&lt;/code>, &lt;code>f5-bigip-…&lt;/code>):&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/kagent-agents-fleet.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-11-telegram-multi-agent-bot/kagent-agents-fleet.png" alt="Solo Enterprise for kagent — the Agents list, every agent Healthy on cluster mgmt-cluster / namespace kagent, with Model column showing OpenAI (gpt-5.5) and OpenAI (Qwen) and Required Tools listing drone-mcp and f5-bigip servers." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>That &lt;code>Model&lt;/code> column is the sleight of hand: it says &amp;ldquo;OpenAI (gpt-5.5)&amp;rdquo; and &amp;ldquo;OpenAI (Qwen),&amp;rdquo; but both are OpenAI-&lt;em>compatible&lt;/em> endpoints that resolve to my gateway — not to OpenAI.&lt;/p>
&lt;p>kagent &lt;code>ModelConfig&lt;/code>s point at in-cluster gateways, not at providers:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>ModelConfig&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Gateway base URL (in-cluster)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>default-model-config&lt;/code>&lt;/td>
&lt;td>lab default&lt;/td>
&lt;td>&lt;code>…/openai/v1&lt;/code> · &lt;code>agentgateway-proxy&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>xai-grok&lt;/code>&lt;/td>
&lt;td>&lt;code>grok-4.5&lt;/code>&lt;/td>
&lt;td>&lt;code>…/grok/v1&lt;/code> · &lt;code>xai-grok-gateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dgx-spark&lt;/code>&lt;/td>
&lt;td>Qwen (local, self-hosted)&lt;/td>
&lt;td>&lt;code>…/spark/v1&lt;/code> · &lt;code>dgx-spark-gateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gpt-5-6&lt;/code>&lt;/td>
&lt;td>&lt;code>gpt-5.6&lt;/code>&lt;/td>
&lt;td>&lt;code>…/gpt56/v1&lt;/code> · dedicated gateway&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The real OpenAI, xAI, and Anthropic keys sit in Vault. External Secrets Operator syncs them into gateway-side Secrets. agentgateway injects them upstream at call time. Which means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>One control plane for spend, traces, and budgets&lt;/strong> — not one per app.&lt;/li>
&lt;li>&lt;strong>Agents are credential-free.&lt;/strong> Compromise a pod, you get no keys.&lt;/li>
&lt;li>&lt;strong>I can swap &lt;code>gpt-5.5&lt;/code> for Qwen-on-my-DGX-Spark&lt;/strong> without touching the bot, the scheduler, or a single agent. It&amp;rsquo;s a one-line &lt;code>ModelConfig&lt;/code> change.&lt;/li>
&lt;li>&lt;strong>Cost and Langfuse stay accurate across channels&lt;/strong> — chat and cron land in the same ledger.&lt;/li>
&lt;/ul>
&lt;p>That last point matters more than it sounds, because the chat bot isn&amp;rsquo;t the only way in.&lt;/p>
&lt;hr>
&lt;h2 id="act-4--the-cron-twin">Act 4 — The cron twin&lt;/h2>
&lt;p>&lt;code>@KagentCorpAIbot&lt;/code> is for &lt;em>&amp;ldquo;help me right now.&amp;rdquo;&lt;/em> But some things should just happen every morning — a cluster health sweep, a firewall device audit, a &amp;ldquo;what PRs are stale&amp;rdquo; report. For that there&amp;rsquo;s a second front-end on the exact same fleet: the &lt;strong>Agent Scheduler&lt;/strong>, and it is pure GitOps.&lt;/p>
&lt;p>Here&amp;rsquo;s the twist I like: the scheduler&amp;rsquo;s web UI &lt;strong>never creates a CronJob through the Kubernetes API.&lt;/strong> It can&amp;rsquo;t set desired state directly. All it does is open a &lt;strong>pull request&lt;/strong> against &lt;code>sebbycorp/k8s-goose&lt;/code>. ArgoCD notices, merges, and applies. Git stays the one source of truth; the cluster only ever &lt;em>executes&lt;/em> what Git says.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> Web UI (:30955) GitHub ArgoCD ns kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌─────────────┐ PR + ┌──────────────┐ sync ┌────────────┐ apply ┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ save a │ ──────▶ │ config/agent-│ ─────▶ │ agentgw- │ ──────▶ │ CronJob │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ schedule │ merge │ schedules/* │ │ config app │ │ ags-&amp;lt;name&amp;gt; │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────────┘ └──────────────┘ └────────────┘ └──────┬───────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ fires
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Job → A2A message/send → agent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> result → ConfigMap ags-result-*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (± Telegram summary, same bot token)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When a scheduled Job fires, it POSTs the same A2A &lt;code>message/send&lt;/code> to the same agent Service the chat bot uses, and writes the reply into ConfigMaps (&lt;code>ags-result-&amp;lt;name&amp;gt;&lt;/code> for the latest, &lt;code>ags-hist-…&lt;/code> for history). Check the box marked &lt;em>&amp;ldquo;send result to Telegram&amp;rdquo;&lt;/em> and the Job posts a summary to my chat using the &lt;strong>same Vault bot token&lt;/strong> — so my morning cluster report shows up in the same window where I do my ad-hoc asks.&lt;/p>
&lt;p>One caveat worth stating plainly: that beautiful HITL approval gate is a &lt;em>problem&lt;/em> for unattended cron. A Job can&amp;rsquo;t tap Approve. So scheduled prompts start read-only — audits, reports, &amp;ldquo;tell me what changed&amp;rdquo; — and the writes stay in the interactive channel where a human thumb is present.&lt;/p>
&lt;hr>
&lt;h2 id="the-shape-of-it">The shape of it&lt;/h2>
&lt;p>Strip away the fun and here&amp;rsquo;s what&amp;rsquo;s actually true:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Two front-ends, one fleet.&lt;/strong> A Telegram bot for conversation, a GitOps scheduler for routines — both are thin edge adapters that speak A2A to the &lt;em>same&lt;/em> in-cluster kagent agents.&lt;/li>
&lt;li>&lt;strong>kagent runs the brains.&lt;/strong> Six agents, each with its own MCP tool servers and its own RBAC/blast-radius, reachable at &lt;code>:8080&lt;/code> over A2A.&lt;/li>
&lt;li>&lt;strong>agentgateway is the governor.&lt;/strong> Every model call is priced, traced, key-injected, and policy-checked before it reaches a provider. Keys live in Vault; nothing sensitive is in Git.&lt;/li>
&lt;li>&lt;strong>My thumb is the last mile.&lt;/strong> Destructive tools stop for an inline Approve/Reject.&lt;/li>
&lt;/ul>
&lt;p>It started as a joke — &lt;em>&amp;ldquo;what if I could text my firewall?&amp;rdquo;&lt;/em> — and turned into the cleanest way I&amp;rsquo;ve found to operate a homelab: fun on the surface, governed all the way down.&lt;/p>
&lt;p>If you want to build your own, the whole thing is GitOps&amp;rsquo;d in &lt;strong>&lt;a href="https://github.com/sebbycorp/k8s-goose">sebbycorp/k8s-goose&lt;/a>&lt;/strong> — bot source in &lt;code>telegram-bot/&lt;/code>, scheduler in &lt;code>agent-scheduler/&lt;/code>, and the operator runbook (seed the Vault bot token, verify the ExternalSecret, smoke-test the chat path) on the live &lt;strong>&lt;a href="https://goose.maniak.ai/dispatch.html">Dispatch page&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>Now if you&amp;rsquo;ll excuse me, I have a drone to fly from the kitchen.&lt;/p></content:encoded></item><item><title>My reMarkable Writes Back — and agentgateway Governs Every Word</title><link>https://maniak.io/articles/2026-07-07-remarkable-agentgateway-write-on-paper-governed/</link><pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-07-remarkable-agentgateway-write-on-paper-governed/</guid><description>&lt;p>Write a question on paper with a pen. Pause. The page &lt;strong>writes back&lt;/strong> — a cursive answer, drawn onto the e-ink letter by letter, in the voice of whichever model you tapped. No screen glare, no keyboard, no page reload. Just ink answering ink.&lt;/p>
&lt;p>That&amp;rsquo;s the &lt;strong>ink desk&lt;/strong> running on my reMarkable 2. It&amp;rsquo;s a single self-contained C binary on the tablet that reads the Wacom pen, grabs the page as a PNG when I pause, has &lt;strong>gpt-5.5&lt;/strong> vision transcribe my handwriting, sends the question to the model I&amp;rsquo;ve selected — OpenAI, Claude, Grok, or my &lt;strong>own local Qwen&lt;/strong> — and renders the reply back onto the panel in a real cursive font.&lt;/p>
&lt;p>Live page: &lt;strong>&lt;a href="http://goose.maniak.ai/remarkable.html">goose.maniak.ai/remarkable.html&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>Here&amp;rsquo;s the twist that matters to me: &lt;strong>the tablet never talks to a model directly.&lt;/strong> Every call goes through &lt;em>my&lt;/em> &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>. The keys live in Vault, every question is a trace in Langfuse, and any request can be &lt;strong>blocked or governed&lt;/strong> by policy before it reaches a provider. This post is two acts — the desk itself, and the gateway that governs it.&lt;/p>
&lt;hr>
&lt;h2 id="act-1--write-on-paper-it-writes-back">Act 1 — Write on paper, it writes back&lt;/h2>
&lt;p>The magic is a five-stage loop that never leaves the tablet except to call the gateway:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Stage&lt;/th>
&lt;th>What happens&lt;/th>
&lt;th>On&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>01&lt;/td>
&lt;td>&lt;strong>WRITE&lt;/strong>&lt;/td>
&lt;td>Reads the Wacom pen and draws your strokes straight to the panel&lt;/td>
&lt;td>&lt;code>evdev · rm2fb&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>02&lt;/td>
&lt;td>&lt;strong>READ&lt;/strong>&lt;/td>
&lt;td>On a short pause it captures the page as a PNG — in-app&lt;/td>
&lt;td>&lt;code>framebuffer → PNG&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>03&lt;/td>
&lt;td>&lt;strong>SEE&lt;/strong>&lt;/td>
&lt;td>gpt-5.5 vision reads the handwriting to text, so &lt;em>any&lt;/em> model can answer&lt;/td>
&lt;td>&lt;code>gpt-5.5 · /openai&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>04&lt;/td>
&lt;td>&lt;strong>ASK&lt;/strong>&lt;/td>
&lt;td>The model you tapped replies — text, a flowchart, or a sketch&lt;/td>
&lt;td>&lt;code>via agentgateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>05&lt;/td>
&lt;td>&lt;strong>INK&lt;/strong>&lt;/td>
&lt;td>Rendered back onto the e-ink in cursive, letter by letter&lt;/td>
&lt;td>&lt;code>stb_truetype · A2&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reMarkable 2 has no usable framebuffer of its own, so the app drives the e-ink through &lt;strong>rm2fb&lt;/strong> (&lt;a href="https://github.com/timower/rM2-stuff">timower/rM2-stuff&lt;/a>) — which means the tablet had to be downgraded to OS 3.22.4.2 (3.27 kept on the spare A/B partition as a fallback). The app is &lt;code>diary.c&lt;/code>, cross-compiled for armv7 with the toltec toolchain, with &lt;strong>zero runtime dependencies&lt;/strong>. Gestures are physical: tap the top-right to &lt;strong>switch models&lt;/strong>, tap the top-left to &lt;strong>save the page&lt;/strong> to your library, and flip the marker to its eraser end + tap to clear and start fresh. Ask it to &lt;em>&amp;ldquo;diagram X&amp;rdquo;&lt;/em> and it auto-lays-out a mermaid-style flowchart on the page.&lt;/p>
&lt;p>Here&amp;rsquo;s the desk answering a real question — &lt;em>&amp;ldquo;what is an agent substrate?&amp;rdquo;&lt;/em> — written by hand:&lt;/p>
&lt;div style="max-width:340px;margin:1.5rem auto;">
&lt;div style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;border-radius:12px;">
&lt;iframe src="https://www.youtube.com/embed/oeFj3B9T8M0" title="Asking a reMarkable 'what is an agent substrate?' — the page writes back in cursive" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen>&lt;/iframe>
&lt;/div>
&lt;/div>
&lt;p>I picked that question on purpose. An &lt;strong>&lt;a href="https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/">agent substrate&lt;/a>&lt;/strong> is the runtime layer that lets stateful agents suspend, resume, and survive — the thing that makes an agent more than a stateless prompt. Watching the tablet &lt;em>hand-write the answer to a question about agent runtimes&lt;/em>, through the same gateway I use for everything else, is the whole point: the substrate, the governance, and the observability follow the agent — even onto paper.&lt;/p>
&lt;hr>
&lt;h2 id="act-2--agentgateway-governs-every-word">Act 2 — agentgateway governs every word&lt;/h2>
&lt;p>The desk is the fun part. The serious part is that &lt;strong>nothing on the tablet is privileged.&lt;/strong> The app just hits OpenAI-compatible routes on the gateway&amp;rsquo;s NodePorts over Wi-Fi:&lt;/p>
&lt;ul>
&lt;li>&lt;code>:/openai&lt;/code> → OpenAI · gpt-5.5 (also the shared &amp;ldquo;eyes&amp;rdquo; that read every page)&lt;/li>
&lt;li>&lt;code>:/anthropic&lt;/code> → Claude · claude-fable-5&lt;/li>
&lt;li>&lt;code>:/grok&lt;/code> → xAI · Grok-4.3&lt;/li>
&lt;li>&lt;code>:/spark&lt;/code> → local &lt;strong>Qwen3.6&lt;/strong> on my own DGX Spark — no cloud, private model, on-paper ink&lt;/li>
&lt;/ul>
&lt;p>agentgateway injects each provider key (sourced from &lt;strong>Vault&lt;/strong> via ESO), records every question as a &lt;strong>Langfuse&lt;/strong> trace — latency, tokens, cost, like any other lab workload — and, crucially, applies policy. Because every answer is a gateway call, I can &lt;strong>govern and block&lt;/strong> requests centrally: rate limits, spend caps, guardrails, and route rules all sit at the gateway, not on a tablet I&amp;rsquo;d have to re-flash to change my mind.&lt;/p>
&lt;p>Here&amp;rsquo;s agentgateway governing and blocking the traffic in real time:&lt;/p>
&lt;div style="max-width:340px;margin:1.5rem auto;">
&lt;div style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;border-radius:12px;">
&lt;iframe src="https://www.youtube.com/embed/amKZZEsimZc" title="agentgateway governing and blocking requests from the reMarkable ink desk" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen>&lt;/iframe>
&lt;/div>
&lt;/div>
&lt;p>Swap a model, tighten a limit, or block a route — and the tablet never changes. That&amp;rsquo;s the difference between &lt;em>a device that calls an API&lt;/em> and &lt;em>an agent that runs behind a governed control plane&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture-end-to-end">The architecture, end to end&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">reMarkable 2 (OS 3.22 · rm2fb drives the e-ink)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pen ─► [ diary.c ] ─► capture page → PNG
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Wi-Fi · agentgateway NodePorts (HTTP)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────── agentgateway ────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ gpt-5.5 reads the handwriting → text │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ selected model answers → text / flowchart │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ key from Vault (ESO) · traces → Langfuse │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ policy: rate limits · spend caps · block │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OpenAI · Claude · Grok · local Qwen (DGX Spark)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> answer written to the e-ink in cursive (letter by letter)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything model-facing is configured in one &lt;code>MODELS[]&lt;/code> table in &lt;code>diary.c&lt;/code>; everything &lt;em>policy&lt;/em>-facing lives at the gateway. The tablet holds no secrets and enforces no rules — it just writes, captures, asks, and inks. Vault holds the keys, Langfuse holds the truth, and agentgateway holds the line.&lt;/p>
&lt;p>The takeaway is small and stubborn: &lt;strong>governance follows the agent everywhere it goes — even to paper.&lt;/strong> Your pen in, a cursive answer in your model&amp;rsquo;s voice back on the page, and every word of it metered, traced, and governable.&lt;/p>
&lt;p>Try it live at &lt;strong>&lt;a href="http://goose.maniak.ai/remarkable.html">goose.maniak.ai/remarkable.html&lt;/a>&lt;/strong>.&lt;/p></description><content:encoded>&lt;p>Write a question on paper with a pen. Pause. The page &lt;strong>writes back&lt;/strong> — a cursive answer, drawn onto the e-ink letter by letter, in the voice of whichever model you tapped. No screen glare, no keyboard, no page reload. Just ink answering ink.&lt;/p>
&lt;p>That&amp;rsquo;s the &lt;strong>ink desk&lt;/strong> running on my reMarkable 2. It&amp;rsquo;s a single self-contained C binary on the tablet that reads the Wacom pen, grabs the page as a PNG when I pause, has &lt;strong>gpt-5.5&lt;/strong> vision transcribe my handwriting, sends the question to the model I&amp;rsquo;ve selected — OpenAI, Claude, Grok, or my &lt;strong>own local Qwen&lt;/strong> — and renders the reply back onto the panel in a real cursive font.&lt;/p>
&lt;p>Live page: &lt;strong>&lt;a href="http://goose.maniak.ai/remarkable.html">goose.maniak.ai/remarkable.html&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>Here&amp;rsquo;s the twist that matters to me: &lt;strong>the tablet never talks to a model directly.&lt;/strong> Every call goes through &lt;em>my&lt;/em> &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>. The keys live in Vault, every question is a trace in Langfuse, and any request can be &lt;strong>blocked or governed&lt;/strong> by policy before it reaches a provider. This post is two acts — the desk itself, and the gateway that governs it.&lt;/p>
&lt;hr>
&lt;h2 id="act-1--write-on-paper-it-writes-back">Act 1 — Write on paper, it writes back&lt;/h2>
&lt;p>The magic is a five-stage loop that never leaves the tablet except to call the gateway:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Stage&lt;/th>
&lt;th>What happens&lt;/th>
&lt;th>On&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>01&lt;/td>
&lt;td>&lt;strong>WRITE&lt;/strong>&lt;/td>
&lt;td>Reads the Wacom pen and draws your strokes straight to the panel&lt;/td>
&lt;td>&lt;code>evdev · rm2fb&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>02&lt;/td>
&lt;td>&lt;strong>READ&lt;/strong>&lt;/td>
&lt;td>On a short pause it captures the page as a PNG — in-app&lt;/td>
&lt;td>&lt;code>framebuffer → PNG&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>03&lt;/td>
&lt;td>&lt;strong>SEE&lt;/strong>&lt;/td>
&lt;td>gpt-5.5 vision reads the handwriting to text, so &lt;em>any&lt;/em> model can answer&lt;/td>
&lt;td>&lt;code>gpt-5.5 · /openai&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>04&lt;/td>
&lt;td>&lt;strong>ASK&lt;/strong>&lt;/td>
&lt;td>The model you tapped replies — text, a flowchart, or a sketch&lt;/td>
&lt;td>&lt;code>via agentgateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>05&lt;/td>
&lt;td>&lt;strong>INK&lt;/strong>&lt;/td>
&lt;td>Rendered back onto the e-ink in cursive, letter by letter&lt;/td>
&lt;td>&lt;code>stb_truetype · A2&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The reMarkable 2 has no usable framebuffer of its own, so the app drives the e-ink through &lt;strong>rm2fb&lt;/strong> (&lt;a href="https://github.com/timower/rM2-stuff">timower/rM2-stuff&lt;/a>) — which means the tablet had to be downgraded to OS 3.22.4.2 (3.27 kept on the spare A/B partition as a fallback). The app is &lt;code>diary.c&lt;/code>, cross-compiled for armv7 with the toltec toolchain, with &lt;strong>zero runtime dependencies&lt;/strong>. Gestures are physical: tap the top-right to &lt;strong>switch models&lt;/strong>, tap the top-left to &lt;strong>save the page&lt;/strong> to your library, and flip the marker to its eraser end + tap to clear and start fresh. Ask it to &lt;em>&amp;ldquo;diagram X&amp;rdquo;&lt;/em> and it auto-lays-out a mermaid-style flowchart on the page.&lt;/p>
&lt;p>Here&amp;rsquo;s the desk answering a real question — &lt;em>&amp;ldquo;what is an agent substrate?&amp;rdquo;&lt;/em> — written by hand:&lt;/p>
&lt;div style="max-width:340px;margin:1.5rem auto;">
&lt;div style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;border-radius:12px;">
&lt;iframe src="https://www.youtube.com/embed/oeFj3B9T8M0" title="Asking a reMarkable 'what is an agent substrate?' — the page writes back in cursive" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen>&lt;/iframe>
&lt;/div>
&lt;/div>
&lt;p>I picked that question on purpose. An &lt;strong>&lt;a href="https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/">agent substrate&lt;/a>&lt;/strong> is the runtime layer that lets stateful agents suspend, resume, and survive — the thing that makes an agent more than a stateless prompt. Watching the tablet &lt;em>hand-write the answer to a question about agent runtimes&lt;/em>, through the same gateway I use for everything else, is the whole point: the substrate, the governance, and the observability follow the agent — even onto paper.&lt;/p>
&lt;hr>
&lt;h2 id="act-2--agentgateway-governs-every-word">Act 2 — agentgateway governs every word&lt;/h2>
&lt;p>The desk is the fun part. The serious part is that &lt;strong>nothing on the tablet is privileged.&lt;/strong> The app just hits OpenAI-compatible routes on the gateway&amp;rsquo;s NodePorts over Wi-Fi:&lt;/p>
&lt;ul>
&lt;li>&lt;code>:/openai&lt;/code> → OpenAI · gpt-5.5 (also the shared &amp;ldquo;eyes&amp;rdquo; that read every page)&lt;/li>
&lt;li>&lt;code>:/anthropic&lt;/code> → Claude · claude-fable-5&lt;/li>
&lt;li>&lt;code>:/grok&lt;/code> → xAI · Grok-4.3&lt;/li>
&lt;li>&lt;code>:/spark&lt;/code> → local &lt;strong>Qwen3.6&lt;/strong> on my own DGX Spark — no cloud, private model, on-paper ink&lt;/li>
&lt;/ul>
&lt;p>agentgateway injects each provider key (sourced from &lt;strong>Vault&lt;/strong> via ESO), records every question as a &lt;strong>Langfuse&lt;/strong> trace — latency, tokens, cost, like any other lab workload — and, crucially, applies policy. Because every answer is a gateway call, I can &lt;strong>govern and block&lt;/strong> requests centrally: rate limits, spend caps, guardrails, and route rules all sit at the gateway, not on a tablet I&amp;rsquo;d have to re-flash to change my mind.&lt;/p>
&lt;p>Here&amp;rsquo;s agentgateway governing and blocking the traffic in real time:&lt;/p>
&lt;div style="max-width:340px;margin:1.5rem auto;">
&lt;div style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;border-radius:12px;">
&lt;iframe src="https://www.youtube.com/embed/amKZZEsimZc" title="agentgateway governing and blocking requests from the reMarkable ink desk" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen>&lt;/iframe>
&lt;/div>
&lt;/div>
&lt;p>Swap a model, tighten a limit, or block a route — and the tablet never changes. That&amp;rsquo;s the difference between &lt;em>a device that calls an API&lt;/em> and &lt;em>an agent that runs behind a governed control plane&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture-end-to-end">The architecture, end to end&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">reMarkable 2 (OS 3.22 · rm2fb drives the e-ink)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> pen ─► [ diary.c ] ─► capture page → PNG
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Wi-Fi · agentgateway NodePorts (HTTP)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────── agentgateway ────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ gpt-5.5 reads the handwriting → text │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ selected model answers → text / flowchart │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ key from Vault (ESO) · traces → Langfuse │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ policy: rate limits · spend caps · block │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OpenAI · Claude · Grok · local Qwen (DGX Spark)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> answer written to the e-ink in cursive (letter by letter)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything model-facing is configured in one &lt;code>MODELS[]&lt;/code> table in &lt;code>diary.c&lt;/code>; everything &lt;em>policy&lt;/em>-facing lives at the gateway. The tablet holds no secrets and enforces no rules — it just writes, captures, asks, and inks. Vault holds the keys, Langfuse holds the truth, and agentgateway holds the line.&lt;/p>
&lt;p>The takeaway is small and stubborn: &lt;strong>governance follows the agent everywhere it goes — even to paper.&lt;/strong> Your pen in, a cursive answer in your model&amp;rsquo;s voice back on the page, and every word of it metered, traced, and governable.&lt;/p>
&lt;p>Try it live at &lt;strong>&lt;a href="http://goose.maniak.ai/remarkable.html">goose.maniak.ai/remarkable.html&lt;/a>&lt;/strong>.&lt;/p></content:encoded></item><item><title>GPT-5.5 vs Claude vs Grok with agentgateway: Which Model Saves Money? (I Made Them Fly a Drone)</title><link>https://maniak.io/articles/2026-07-06-drone-agent-mcp-tool-modes-standard-vs-codesearch/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-06-drone-agent-mcp-tool-modes-standard-vs-codesearch/</guid><description>&lt;p>Same job, three brains. I gave an AI agent a real &lt;strong>Ryze RoboMaster TT&lt;/strong> drone and one fixed mission — &lt;em>take off, flip, photograph the room, spin 360°, land and report&lt;/em> — then flew it &lt;strong>identically on three models&lt;/strong>: &lt;code>gpt-5.5&lt;/code>, &lt;code>claude-fable-5&lt;/code>, and &lt;code>grok-4.3&lt;/code>. Same agent, same drone, same 28-tool MCP server, same prompts. The only thing that changed was the model, swapped in with a &lt;strong>one-line &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> ModelConfig&lt;/strong>.&lt;/p>
&lt;p>The bill for that identical flight ranged &lt;strong>5× from cheapest to dearest.&lt;/strong> This post is about who saves money, why, and the one extra knob that moves the answer. Every number is measured — pulled from kagent&amp;rsquo;s tracing and Langfuse, not estimated.&lt;/p>
&lt;p>Live flight deck: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>Here&amp;rsquo;s the agent flying the mission on the real drone:&lt;/p>
&lt;video controls playsinline muted preload="metadata" style="width:100%;height:auto;border-radius:8px" aria-label="The AI agent flying the identical mission on a real Ryze RoboMaster TT drone">
&lt;source src="https://maniak.io/videos/2026-07-06-drone-flight.mp4" type="video/mp4">
Your browser does not support the video tag.
&lt;/video>
&lt;h2 id="the-verdict-cost-per-flight">The verdict: cost per flight&lt;/h2>
&lt;svg viewBox="0 0 720 320" role="img" aria-label="Cost per flight — Standard vs CodeSearch, three models" style="width:100%;height:auto">
&lt;text x="54" y="26" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="13" font-weight="700">Cost per flight — Standard vs CodeSearch, three models&lt;/text>
&lt;rect x="470" y="16" width="11" height="11" rx="2" fill="#9d7bff"/>&lt;text x="486" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">Standard&lt;/text>
&lt;rect x="570" y="16" width="11" height="11" rx="2" fill="#46e0c0"/>&lt;text x="586" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">CodeSearch&lt;/text>
&lt;line x1="54" y1="266.0" x2="704" y2="266.0" stroke="#1c2a3d"/>
&lt;text x="48" y="269.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$0.00&lt;/text>
&lt;line x1="54" y1="213.0" x2="704" y2="213.0" stroke="#1c2a3d"/>
&lt;text x="48" y="216.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$0.45&lt;/text>
&lt;line x1="54" y1="160.0" x2="704" y2="160.0" stroke="#1c2a3d"/>
&lt;text x="48" y="163.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$0.90&lt;/text>
&lt;line x1="54" y1="107.0" x2="704" y2="107.0" stroke="#1c2a3d"/>
&lt;text x="48" y="110.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$1.35&lt;/text>
&lt;line x1="54" y1="54.0" x2="704" y2="54.0" stroke="#1c2a3d"/>
&lt;text x="48" y="57.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$1.80&lt;/text>
&lt;rect x="90.8" y="204.1" width="60.7" height="61.9" rx="3" fill="#9d7bff"/>
&lt;text x="121.2" y="199.1" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.53&lt;/text>
&lt;rect x="173.2" y="228.3" width="60.7" height="37.7" rx="3" fill="#46e0c0"/>
&lt;text x="203.5" y="223.3" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.32&lt;/text>
&lt;text x="162.3" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">gpt-5.5&lt;/text>
&lt;rect x="307.5" y="69.6" width="60.7" height="196.4" rx="3" fill="#9d7bff"/>
&lt;text x="337.8" y="64.6" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$1.67&lt;/text>
&lt;rect x="389.8" y="87.4" width="60.7" height="178.6" rx="3" fill="#46e0c0"/>
&lt;text x="420.2" y="82.4" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$1.52&lt;/text>
&lt;text x="379.0" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">claude-fable-5&lt;/text>
&lt;rect x="524.2" y="225.5" width="60.7" height="40.5" rx="3" fill="#9d7bff"/>
&lt;text x="554.5" y="220.5" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.34&lt;/text>
&lt;rect x="606.5" y="230.1" width="60.7" height="35.9" rx="3" fill="#46e0c0"/>
&lt;text x="636.8" y="225.1" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.30&lt;/text>
&lt;text x="595.7" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">grok-4.3&lt;/text>
&lt;text x="54" y="314" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9">cost per full flight (USD) · lower is better&lt;/text>
&lt;/svg>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Tokens&lt;/th>
&lt;th style="text-align:right">Latency&lt;/th>
&lt;th style="text-align:right">Cost&lt;/th>
&lt;th style="text-align:right">CodeSearch saves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>grok-4.3&lt;/strong> 🏆&lt;/td>
&lt;td>CodeSearch&lt;/td>
&lt;td style="text-align:right">99.5k&lt;/td>
&lt;td style="text-align:right">31s&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.3050&lt;/strong>&lt;/td>
&lt;td style="text-align:right">−11%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>grok-4.3&lt;/td>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">113.0k&lt;/td>
&lt;td style="text-align:right">25s&lt;/td>
&lt;td style="text-align:right">$0.3441&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gpt-5.5&lt;/td>
&lt;td>CodeSearch&lt;/td>
&lt;td style="text-align:right">53.1k&lt;/td>
&lt;td style="text-align:right">37s&lt;/td>
&lt;td style="text-align:right">$0.3201&lt;/td>
&lt;td style="text-align:right">&lt;strong>−39%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gpt-5.5&lt;/td>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">98.3k&lt;/td>
&lt;td style="text-align:right">47s&lt;/td>
&lt;td style="text-align:right">$0.5252&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>claude-fable-5&lt;/td>
&lt;td>CodeSearch&lt;/td>
&lt;td style="text-align:right">165.6k&lt;/td>
&lt;td style="text-align:right">101s&lt;/td>
&lt;td style="text-align:right">$1.5167&lt;/td>
&lt;td style="text-align:right">−9%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>claude-fable-5&lt;/td>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">159.5k&lt;/td>
&lt;td style="text-align:right">79s&lt;/td>
&lt;td style="text-align:right">$1.6676&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>CodeSearch saves&lt;/strong> = each model&amp;rsquo;s CodeSearch run vs &lt;em>its own&lt;/em> Standard run — the tool mode&amp;rsquo;s effect on that model. It&amp;rsquo;s a big lever for gpt-5.5 (−39%) but small for grok (−11%) and Claude (−9%). The cross-model &lt;em>cost&lt;/em> winner is grok either way — it&amp;rsquo;s ~1/5th of Claude&amp;rsquo;s bill, but that&amp;rsquo;s the &lt;strong>model&lt;/strong> choice, not the tool mode.&lt;/p>
&lt;/blockquote>
&lt;p>Three findings, in order of how much money they save you:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>grok-4.3 is the cheapest &lt;em>and&lt;/em> the fastest.&lt;/strong> ~$0.30–0.34 a flight, done in ~25–31s. It&amp;rsquo;s almost comically terse — asked to flip, it replied &lt;em>&amp;ldquo;Forward flip completed.&amp;rdquo;&lt;/em> — &lt;strong>4 output tokens.&lt;/strong> Cheap talk, literally.&lt;/li>
&lt;li>&lt;strong>gpt-5.5 is right behind on cost and the leanest on tokens&lt;/strong> ($0.32–0.53, 53–98k tokens). It&amp;rsquo;s the model that responds best to the tool-mode trick below.&lt;/li>
&lt;li>&lt;strong>claude-fable-5 is the premium option&lt;/strong> — the most thorough answers, but &lt;strong>~5× grok&amp;rsquo;s cost and ~3× gpt-5.5&amp;rsquo;s&lt;/strong> for the exact same mission. Great model; you pay for it.&lt;/li>
&lt;/ul>
&lt;p>If the only thing you care about is the bill, &lt;strong>grok wins, gpt-5.5 is a close and leaner second, Claude is a deliberate splurge.&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>Cost = tokens × each model&amp;rsquo;s price. gpt-5.5 and Claude are priced by Langfuse; grok-4.3 I priced from &lt;a href="https://models.dev">models.dev&lt;/a> at the grok-4 rate ($3/M in, $15/M out).&lt;/p>
&lt;/blockquote>
&lt;h2 id="why-the-gap-is-so-wide">Why the gap is so wide&lt;/h2>
&lt;p>It isn&amp;rsquo;t just per-token price — it&amp;rsquo;s &lt;strong>how much each model says&lt;/strong>. Claude reasons out loud and writes verbose tool-discovery code, so it burns 160k+ tokens on a flight. grok is monosyllabic. gpt-5.5 sits in between but is disciplined. Two models can complete the identical mission and differ 5× on the bill purely from verbosity × price. You only see that if you meter it — which is the whole reason agentgateway sits in the middle.&lt;/p>
&lt;h2 id="the-second-lever-mcp-tool-mode">The second lever: MCP tool mode&lt;/h2>
&lt;p>There&amp;rsquo;s a knob &lt;em>within&lt;/em> each model. Every MCP call injects the &lt;strong>tool catalog&lt;/strong> — the schema of all 28 tools — into the prompt. agentgateway can hide that behind two modes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Standard&lt;/strong> — all 28 tools, full schemas, every turn.&lt;/li>
&lt;li>&lt;strong>CodeSearch&lt;/strong> — two meta-tools (&lt;code>get_tool&lt;/code> to fetch one signature on demand, &lt;code>run_code&lt;/code> to batch calls in a JS sandbox). Tiny context, fewer round-trips.&lt;/li>
&lt;/ul>
&lt;p>Here&amp;rsquo;s tokens per flight, both modes, all three models:&lt;/p>
&lt;svg viewBox="0 0 720 320" role="img" aria-label="Tokens per flight — Standard vs CodeSearch, three models" style="width:100%;height:auto">
&lt;text x="54" y="26" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="13" font-weight="700">Tokens per flight — Standard vs CodeSearch, three models&lt;/text>
&lt;rect x="470" y="16" width="11" height="11" rx="2" fill="#9d7bff"/>&lt;text x="486" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">Standard&lt;/text>
&lt;rect x="570" y="16" width="11" height="11" rx="2" fill="#46e0c0"/>&lt;text x="586" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">CodeSearch&lt;/text>
&lt;line x1="54" y1="266.0" x2="704" y2="266.0" stroke="#1c2a3d"/>
&lt;text x="48" y="269.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">0k&lt;/text>
&lt;line x1="54" y1="213.0" x2="704" y2="213.0" stroke="#1c2a3d"/>
&lt;text x="48" y="216.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">45k&lt;/text>
&lt;line x1="54" y1="160.0" x2="704" y2="160.0" stroke="#1c2a3d"/>
&lt;text x="48" y="163.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">90k&lt;/text>
&lt;line x1="54" y1="107.0" x2="704" y2="107.0" stroke="#1c2a3d"/>
&lt;text x="48" y="110.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">135k&lt;/text>
&lt;line x1="54" y1="54.0" x2="704" y2="54.0" stroke="#1c2a3d"/>
&lt;text x="48" y="57.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">180k&lt;/text>
&lt;rect x="90.8" y="150.2" width="60.7" height="115.8" rx="3" fill="#9d7bff"/>
&lt;text x="121.2" y="145.2" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">98k&lt;/text>
&lt;rect x="173.2" y="203.5" width="60.7" height="62.5" rx="3" fill="#46e0c0"/>
&lt;text x="203.5" y="198.5" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">53k&lt;/text>
&lt;text x="162.3" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">gpt-5.5&lt;/text>
&lt;rect x="307.5" y="78.1" width="60.7" height="187.9" rx="3" fill="#9d7bff"/>
&lt;text x="337.8" y="73.1" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">160k&lt;/text>
&lt;rect x="389.8" y="71.0" width="60.7" height="195.0" rx="3" fill="#46e0c0"/>
&lt;text x="420.2" y="66.0" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">166k&lt;/text>
&lt;text x="379.0" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">claude-fable-5&lt;/text>
&lt;rect x="524.2" y="132.9" width="60.7" height="133.1" rx="3" fill="#9d7bff"/>
&lt;text x="554.5" y="127.9" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">113k&lt;/text>
&lt;rect x="606.5" y="148.8" width="60.7" height="117.2" rx="3" fill="#46e0c0"/>
&lt;text x="636.8" y="143.8" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">100k&lt;/text>
&lt;text x="595.7" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">grok-4.3&lt;/text>
&lt;text x="54" y="314" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9">total tokens per flight (thousands) · lower is better&lt;/text>
&lt;/svg>
&lt;p>The catch: &lt;strong>the tool mode that saves money is model-dependent.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>On &lt;strong>gpt-5.5&lt;/strong>, CodeSearch cut tokens &lt;strong>~46%&lt;/strong> and cost &lt;strong>~39%&lt;/strong> ($0.5252 → $0.3201). Big win.&lt;/li>
&lt;li>On &lt;strong>grok&lt;/strong>, a modest trim (it&amp;rsquo;s already terse).&lt;/li>
&lt;li>On &lt;strong>Claude&lt;/strong>, CodeSearch &lt;strong>did nothing&lt;/strong> — its verbose &lt;code>run_code&lt;/code>/discovery steps used &lt;em>more&lt;/em> tokens than Standard. The saving was cancelled out.&lt;/li>
&lt;/ul>
&lt;p>So you can&amp;rsquo;t pick a tool mode in the abstract. &lt;strong>Benchmark the mode with the model you&amp;rsquo;ll actually ship.&lt;/strong> For gpt-5.5, flip to CodeSearch. For Claude, don&amp;rsquo;t bother — spend your savings on picking a cheaper model instead.&lt;/p>
&lt;h3 id="where-gpt-55s-savings-come-from">Where gpt-5.5&amp;rsquo;s savings come from&lt;/h3>
&lt;p>One prompt dominates — the status report, because the agent polls &lt;code>get_state&lt;/code> repeatedly:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Prompt&lt;/th>
&lt;th>Standard (calls / tokens)&lt;/th>
&lt;th>CodeSearch (calls / tokens)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>&lt;strong>Land + status report&lt;/strong>&lt;/td>
&lt;td>&lt;strong>17 / 45.1k&lt;/strong>&lt;/td>
&lt;td>5 / 13.3k&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Standard burns 17 calls and 45k tokens on that one turn; CodeSearch collapses it into a single &lt;code>run_code&lt;/code> that reads state once — 5 calls, 13k tokens. That&amp;rsquo;s most of the −39%.&lt;/p>
&lt;h2 id="the-receipts--kagent-tracing">The receipts — kagent tracing&lt;/h2>
&lt;p>None of this is estimated. kagent (Solo Enterprise for kagent) traces every flight — one trace per prompt, with input, output, duration, and tokens — and exports the same spans to Langfuse:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-tracing-list.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-tracing-list.png" alt="kagent Tracing list: drone_agent flight traces, each a prompt (Take off, Do a flip, Take a photo, Spin 360, Land &amp;#43; status) with duration ~0.7–2.4s and ~2.8k–3.8k tokens per trace." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Drill into a trace and you see the execution flow and every tool call. CodeSearch on Claude — &lt;code>get_tool&lt;/code> then &lt;code>run_code&lt;/code> to take the photo:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-claude.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-claude.png" alt="kagent trace detail for drone_agent_cs_claude: Execution Flow shows get_tool then run_code; Trace Tree shows call_llm → generate_content (claude-fable-5) → openai.chat → execute_tool; 5,623 tokens for the turn." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Claude on Standard, doing the 360° via the &lt;code>celebrate&lt;/code> tool and reading &lt;code>get_state&lt;/code>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-claude.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-claude.png" alt="kagent trace detail for drone_agent_claude: celebrate &amp;#43; get_state tools; output reports a full 360° spin, 80cm, battery 75%; 6,997 tokens." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And grok on CodeSearch — same shape, but look how little it says (4 output tokens — that&amp;rsquo;s why grok&amp;rsquo;s bill is tiny):&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-xai.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-xai.png" alt="kagent trace detail for drone_agent_cs_xai: get_tool &amp;#43; run_code on grok-4.3; the Do-a-flip turn returns just &amp;amp;lsquo;Forward flip completed&amp;amp;rsquo; — 4 output tokens, 2,933 total." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="the-gateway-sees--and-prices--every-hop">The gateway sees — and prices — every hop&lt;/h2>
&lt;p>agentgateway routes each MCP call to the right backend and tags it with the tool mode, method, status, and latency — which is exactly why the cost numbers exist:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">info request gateway=virtual-mcp-gateway route=drone-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> http.method=POST http.path=/drone http.status=200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> protocol=mcp mcp.method.name=tools/list duration=3ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">info request gateway=virtual-mcp-gateway route=drone-mcp-codesearch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> http.method=POST http.path=/drone-codesearch http.status=200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> protocol=mcp mcp.method.name=tools/list duration=2ms
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Because the same proxy fronts both the models and the MCP server, it meters both sides — one place to compare gpt-5.5, Claude, and grok on an apples-to-apples flight, and to swap between them with a config change.&lt;/p>
&lt;h2 id="the-money-lesson">The money lesson&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Pick the model first — it&amp;rsquo;s the 5× lever.&lt;/strong> grok-4.3 flew the mission for ~1/5th the cost of Claude and finished 3× faster; gpt-5.5 is the leaner near-tie. Claude is a deliberate premium.&lt;/li>
&lt;li>&lt;strong>Then tune the tool mode for that model.&lt;/strong> CodeSearch saves gpt-5.5 ~39%; it does nothing for Claude.&lt;/li>
&lt;li>&lt;strong>Meter everything.&lt;/strong> Two models can finish the identical job and differ 5× on cost from verbosity alone. agentgateway makes both the swap and the measurement a one-liner.&lt;/li>
&lt;/ol>
&lt;p>Full flight, all three models, the charts, and the traces live on the flight deck: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>.&lt;/p></description><content:encoded>&lt;p>Same job, three brains. I gave an AI agent a real &lt;strong>Ryze RoboMaster TT&lt;/strong> drone and one fixed mission — &lt;em>take off, flip, photograph the room, spin 360°, land and report&lt;/em> — then flew it &lt;strong>identically on three models&lt;/strong>: &lt;code>gpt-5.5&lt;/code>, &lt;code>claude-fable-5&lt;/code>, and &lt;code>grok-4.3&lt;/code>. Same agent, same drone, same 28-tool MCP server, same prompts. The only thing that changed was the model, swapped in with a &lt;strong>one-line &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> ModelConfig&lt;/strong>.&lt;/p>
&lt;p>The bill for that identical flight ranged &lt;strong>5× from cheapest to dearest.&lt;/strong> This post is about who saves money, why, and the one extra knob that moves the answer. Every number is measured — pulled from kagent&amp;rsquo;s tracing and Langfuse, not estimated.&lt;/p>
&lt;p>Live flight deck: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>Here&amp;rsquo;s the agent flying the mission on the real drone:&lt;/p>
&lt;video controls playsinline muted preload="metadata" style="width:100%;height:auto;border-radius:8px" aria-label="The AI agent flying the identical mission on a real Ryze RoboMaster TT drone">
&lt;source src="https://maniak.io/videos/2026-07-06-drone-flight.mp4" type="video/mp4">
Your browser does not support the video tag.
&lt;/video>
&lt;h2 id="the-verdict-cost-per-flight">The verdict: cost per flight&lt;/h2>
&lt;svg viewBox="0 0 720 320" role="img" aria-label="Cost per flight — Standard vs CodeSearch, three models" style="width:100%;height:auto">
&lt;text x="54" y="26" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="13" font-weight="700">Cost per flight — Standard vs CodeSearch, three models&lt;/text>
&lt;rect x="470" y="16" width="11" height="11" rx="2" fill="#9d7bff"/>&lt;text x="486" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">Standard&lt;/text>
&lt;rect x="570" y="16" width="11" height="11" rx="2" fill="#46e0c0"/>&lt;text x="586" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">CodeSearch&lt;/text>
&lt;line x1="54" y1="266.0" x2="704" y2="266.0" stroke="#1c2a3d"/>
&lt;text x="48" y="269.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$0.00&lt;/text>
&lt;line x1="54" y1="213.0" x2="704" y2="213.0" stroke="#1c2a3d"/>
&lt;text x="48" y="216.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$0.45&lt;/text>
&lt;line x1="54" y1="160.0" x2="704" y2="160.0" stroke="#1c2a3d"/>
&lt;text x="48" y="163.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$0.90&lt;/text>
&lt;line x1="54" y1="107.0" x2="704" y2="107.0" stroke="#1c2a3d"/>
&lt;text x="48" y="110.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$1.35&lt;/text>
&lt;line x1="54" y1="54.0" x2="704" y2="54.0" stroke="#1c2a3d"/>
&lt;text x="48" y="57.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">$1.80&lt;/text>
&lt;rect x="90.8" y="204.1" width="60.7" height="61.9" rx="3" fill="#9d7bff"/>
&lt;text x="121.2" y="199.1" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.53&lt;/text>
&lt;rect x="173.2" y="228.3" width="60.7" height="37.7" rx="3" fill="#46e0c0"/>
&lt;text x="203.5" y="223.3" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.32&lt;/text>
&lt;text x="162.3" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">gpt-5.5&lt;/text>
&lt;rect x="307.5" y="69.6" width="60.7" height="196.4" rx="3" fill="#9d7bff"/>
&lt;text x="337.8" y="64.6" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$1.67&lt;/text>
&lt;rect x="389.8" y="87.4" width="60.7" height="178.6" rx="3" fill="#46e0c0"/>
&lt;text x="420.2" y="82.4" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$1.52&lt;/text>
&lt;text x="379.0" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">claude-fable-5&lt;/text>
&lt;rect x="524.2" y="225.5" width="60.7" height="40.5" rx="3" fill="#9d7bff"/>
&lt;text x="554.5" y="220.5" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.34&lt;/text>
&lt;rect x="606.5" y="230.1" width="60.7" height="35.9" rx="3" fill="#46e0c0"/>
&lt;text x="636.8" y="225.1" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">$0.30&lt;/text>
&lt;text x="595.7" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">grok-4.3&lt;/text>
&lt;text x="54" y="314" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9">cost per full flight (USD) · lower is better&lt;/text>
&lt;/svg>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Tokens&lt;/th>
&lt;th style="text-align:right">Latency&lt;/th>
&lt;th style="text-align:right">Cost&lt;/th>
&lt;th style="text-align:right">CodeSearch saves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>grok-4.3&lt;/strong> 🏆&lt;/td>
&lt;td>CodeSearch&lt;/td>
&lt;td style="text-align:right">99.5k&lt;/td>
&lt;td style="text-align:right">31s&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.3050&lt;/strong>&lt;/td>
&lt;td style="text-align:right">−11%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>grok-4.3&lt;/td>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">113.0k&lt;/td>
&lt;td style="text-align:right">25s&lt;/td>
&lt;td style="text-align:right">$0.3441&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gpt-5.5&lt;/td>
&lt;td>CodeSearch&lt;/td>
&lt;td style="text-align:right">53.1k&lt;/td>
&lt;td style="text-align:right">37s&lt;/td>
&lt;td style="text-align:right">$0.3201&lt;/td>
&lt;td style="text-align:right">&lt;strong>−39%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gpt-5.5&lt;/td>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">98.3k&lt;/td>
&lt;td style="text-align:right">47s&lt;/td>
&lt;td style="text-align:right">$0.5252&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>claude-fable-5&lt;/td>
&lt;td>CodeSearch&lt;/td>
&lt;td style="text-align:right">165.6k&lt;/td>
&lt;td style="text-align:right">101s&lt;/td>
&lt;td style="text-align:right">$1.5167&lt;/td>
&lt;td style="text-align:right">−9%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>claude-fable-5&lt;/td>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">159.5k&lt;/td>
&lt;td style="text-align:right">79s&lt;/td>
&lt;td style="text-align:right">$1.6676&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>CodeSearch saves&lt;/strong> = each model&amp;rsquo;s CodeSearch run vs &lt;em>its own&lt;/em> Standard run — the tool mode&amp;rsquo;s effect on that model. It&amp;rsquo;s a big lever for gpt-5.5 (−39%) but small for grok (−11%) and Claude (−9%). The cross-model &lt;em>cost&lt;/em> winner is grok either way — it&amp;rsquo;s ~1/5th of Claude&amp;rsquo;s bill, but that&amp;rsquo;s the &lt;strong>model&lt;/strong> choice, not the tool mode.&lt;/p>
&lt;/blockquote>
&lt;p>Three findings, in order of how much money they save you:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>grok-4.3 is the cheapest &lt;em>and&lt;/em> the fastest.&lt;/strong> ~$0.30–0.34 a flight, done in ~25–31s. It&amp;rsquo;s almost comically terse — asked to flip, it replied &lt;em>&amp;ldquo;Forward flip completed.&amp;rdquo;&lt;/em> — &lt;strong>4 output tokens.&lt;/strong> Cheap talk, literally.&lt;/li>
&lt;li>&lt;strong>gpt-5.5 is right behind on cost and the leanest on tokens&lt;/strong> ($0.32–0.53, 53–98k tokens). It&amp;rsquo;s the model that responds best to the tool-mode trick below.&lt;/li>
&lt;li>&lt;strong>claude-fable-5 is the premium option&lt;/strong> — the most thorough answers, but &lt;strong>~5× grok&amp;rsquo;s cost and ~3× gpt-5.5&amp;rsquo;s&lt;/strong> for the exact same mission. Great model; you pay for it.&lt;/li>
&lt;/ul>
&lt;p>If the only thing you care about is the bill, &lt;strong>grok wins, gpt-5.5 is a close and leaner second, Claude is a deliberate splurge.&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>Cost = tokens × each model&amp;rsquo;s price. gpt-5.5 and Claude are priced by Langfuse; grok-4.3 I priced from &lt;a href="https://models.dev">models.dev&lt;/a> at the grok-4 rate ($3/M in, $15/M out).&lt;/p>
&lt;/blockquote>
&lt;h2 id="why-the-gap-is-so-wide">Why the gap is so wide&lt;/h2>
&lt;p>It isn&amp;rsquo;t just per-token price — it&amp;rsquo;s &lt;strong>how much each model says&lt;/strong>. Claude reasons out loud and writes verbose tool-discovery code, so it burns 160k+ tokens on a flight. grok is monosyllabic. gpt-5.5 sits in between but is disciplined. Two models can complete the identical mission and differ 5× on the bill purely from verbosity × price. You only see that if you meter it — which is the whole reason agentgateway sits in the middle.&lt;/p>
&lt;h2 id="the-second-lever-mcp-tool-mode">The second lever: MCP tool mode&lt;/h2>
&lt;p>There&amp;rsquo;s a knob &lt;em>within&lt;/em> each model. Every MCP call injects the &lt;strong>tool catalog&lt;/strong> — the schema of all 28 tools — into the prompt. agentgateway can hide that behind two modes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Standard&lt;/strong> — all 28 tools, full schemas, every turn.&lt;/li>
&lt;li>&lt;strong>CodeSearch&lt;/strong> — two meta-tools (&lt;code>get_tool&lt;/code> to fetch one signature on demand, &lt;code>run_code&lt;/code> to batch calls in a JS sandbox). Tiny context, fewer round-trips.&lt;/li>
&lt;/ul>
&lt;p>Here&amp;rsquo;s tokens per flight, both modes, all three models:&lt;/p>
&lt;svg viewBox="0 0 720 320" role="img" aria-label="Tokens per flight — Standard vs CodeSearch, three models" style="width:100%;height:auto">
&lt;text x="54" y="26" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="13" font-weight="700">Tokens per flight — Standard vs CodeSearch, three models&lt;/text>
&lt;rect x="470" y="16" width="11" height="11" rx="2" fill="#9d7bff"/>&lt;text x="486" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">Standard&lt;/text>
&lt;rect x="570" y="16" width="11" height="11" rx="2" fill="#46e0c0"/>&lt;text x="586" y="26" fill="#7788a1" font-family="ui-monospace,monospace" font-size="11">CodeSearch&lt;/text>
&lt;line x1="54" y1="266.0" x2="704" y2="266.0" stroke="#1c2a3d"/>
&lt;text x="48" y="269.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">0k&lt;/text>
&lt;line x1="54" y1="213.0" x2="704" y2="213.0" stroke="#1c2a3d"/>
&lt;text x="48" y="216.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">45k&lt;/text>
&lt;line x1="54" y1="160.0" x2="704" y2="160.0" stroke="#1c2a3d"/>
&lt;text x="48" y="163.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">90k&lt;/text>
&lt;line x1="54" y1="107.0" x2="704" y2="107.0" stroke="#1c2a3d"/>
&lt;text x="48" y="110.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">135k&lt;/text>
&lt;line x1="54" y1="54.0" x2="704" y2="54.0" stroke="#1c2a3d"/>
&lt;text x="48" y="57.0" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9" text-anchor="end">180k&lt;/text>
&lt;rect x="90.8" y="150.2" width="60.7" height="115.8" rx="3" fill="#9d7bff"/>
&lt;text x="121.2" y="145.2" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">98k&lt;/text>
&lt;rect x="173.2" y="203.5" width="60.7" height="62.5" rx="3" fill="#46e0c0"/>
&lt;text x="203.5" y="198.5" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">53k&lt;/text>
&lt;text x="162.3" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">gpt-5.5&lt;/text>
&lt;rect x="307.5" y="78.1" width="60.7" height="187.9" rx="3" fill="#9d7bff"/>
&lt;text x="337.8" y="73.1" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">160k&lt;/text>
&lt;rect x="389.8" y="71.0" width="60.7" height="195.0" rx="3" fill="#46e0c0"/>
&lt;text x="420.2" y="66.0" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">166k&lt;/text>
&lt;text x="379.0" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">claude-fable-5&lt;/text>
&lt;rect x="524.2" y="132.9" width="60.7" height="133.1" rx="3" fill="#9d7bff"/>
&lt;text x="554.5" y="127.9" fill="#9d7bff" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">113k&lt;/text>
&lt;rect x="606.5" y="148.8" width="60.7" height="117.2" rx="3" fill="#46e0c0"/>
&lt;text x="636.8" y="143.8" fill="#46e0c0" font-family="ui-monospace,monospace" font-size="10" text-anchor="middle" font-weight="600">100k&lt;/text>
&lt;text x="595.7" y="284.0" fill="#e4ecf7" font-family="ui-monospace,monospace" font-size="11" text-anchor="middle">grok-4.3&lt;/text>
&lt;text x="54" y="314" fill="#4c5c76" font-family="ui-monospace,monospace" font-size="9">total tokens per flight (thousands) · lower is better&lt;/text>
&lt;/svg>
&lt;p>The catch: &lt;strong>the tool mode that saves money is model-dependent.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>On &lt;strong>gpt-5.5&lt;/strong>, CodeSearch cut tokens &lt;strong>~46%&lt;/strong> and cost &lt;strong>~39%&lt;/strong> ($0.5252 → $0.3201). Big win.&lt;/li>
&lt;li>On &lt;strong>grok&lt;/strong>, a modest trim (it&amp;rsquo;s already terse).&lt;/li>
&lt;li>On &lt;strong>Claude&lt;/strong>, CodeSearch &lt;strong>did nothing&lt;/strong> — its verbose &lt;code>run_code&lt;/code>/discovery steps used &lt;em>more&lt;/em> tokens than Standard. The saving was cancelled out.&lt;/li>
&lt;/ul>
&lt;p>So you can&amp;rsquo;t pick a tool mode in the abstract. &lt;strong>Benchmark the mode with the model you&amp;rsquo;ll actually ship.&lt;/strong> For gpt-5.5, flip to CodeSearch. For Claude, don&amp;rsquo;t bother — spend your savings on picking a cheaper model instead.&lt;/p>
&lt;h3 id="where-gpt-55s-savings-come-from">Where gpt-5.5&amp;rsquo;s savings come from&lt;/h3>
&lt;p>One prompt dominates — the status report, because the agent polls &lt;code>get_state&lt;/code> repeatedly:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Prompt&lt;/th>
&lt;th>Standard (calls / tokens)&lt;/th>
&lt;th>CodeSearch (calls / tokens)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>&lt;strong>Land + status report&lt;/strong>&lt;/td>
&lt;td>&lt;strong>17 / 45.1k&lt;/strong>&lt;/td>
&lt;td>5 / 13.3k&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Standard burns 17 calls and 45k tokens on that one turn; CodeSearch collapses it into a single &lt;code>run_code&lt;/code> that reads state once — 5 calls, 13k tokens. That&amp;rsquo;s most of the −39%.&lt;/p>
&lt;h2 id="the-receipts--kagent-tracing">The receipts — kagent tracing&lt;/h2>
&lt;p>None of this is estimated. kagent (Solo Enterprise for kagent) traces every flight — one trace per prompt, with input, output, duration, and tokens — and exports the same spans to Langfuse:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-tracing-list.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-tracing-list.png" alt="kagent Tracing list: drone_agent flight traces, each a prompt (Take off, Do a flip, Take a photo, Spin 360, Land &amp;#43; status) with duration ~0.7–2.4s and ~2.8k–3.8k tokens per trace." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Drill into a trace and you see the execution flow and every tool call. CodeSearch on Claude — &lt;code>get_tool&lt;/code> then &lt;code>run_code&lt;/code> to take the photo:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-claude.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-claude.png" alt="kagent trace detail for drone_agent_cs_claude: Execution Flow shows get_tool then run_code; Trace Tree shows call_llm → generate_content (claude-fable-5) → openai.chat → execute_tool; 5,623 tokens for the turn." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Claude on Standard, doing the 360° via the &lt;code>celebrate&lt;/code> tool and reading &lt;code>get_state&lt;/code>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-claude.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-claude.png" alt="kagent trace detail for drone_agent_claude: celebrate &amp;#43; get_state tools; output reports a full 360° spin, 80cm, battery 75%; 6,997 tokens." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And grok on CodeSearch — same shape, but look how little it says (4 output tokens — that&amp;rsquo;s why grok&amp;rsquo;s bill is tiny):&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-xai.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-06-drone-tool-modes/kagent-trace-cs-xai.png" alt="kagent trace detail for drone_agent_cs_xai: get_tool &amp;#43; run_code on grok-4.3; the Do-a-flip turn returns just &amp;amp;lsquo;Forward flip completed&amp;amp;rsquo; — 4 output tokens, 2,933 total." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="the-gateway-sees--and-prices--every-hop">The gateway sees — and prices — every hop&lt;/h2>
&lt;p>agentgateway routes each MCP call to the right backend and tags it with the tool mode, method, status, and latency — which is exactly why the cost numbers exist:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">info request gateway=virtual-mcp-gateway route=drone-mcp
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> http.method=POST http.path=/drone http.status=200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> protocol=mcp mcp.method.name=tools/list duration=3ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">info request gateway=virtual-mcp-gateway route=drone-mcp-codesearch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> http.method=POST http.path=/drone-codesearch http.status=200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> protocol=mcp mcp.method.name=tools/list duration=2ms
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Because the same proxy fronts both the models and the MCP server, it meters both sides — one place to compare gpt-5.5, Claude, and grok on an apples-to-apples flight, and to swap between them with a config change.&lt;/p>
&lt;h2 id="the-money-lesson">The money lesson&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Pick the model first — it&amp;rsquo;s the 5× lever.&lt;/strong> grok-4.3 flew the mission for ~1/5th the cost of Claude and finished 3× faster; gpt-5.5 is the leaner near-tie. Claude is a deliberate premium.&lt;/li>
&lt;li>&lt;strong>Then tune the tool mode for that model.&lt;/strong> CodeSearch saves gpt-5.5 ~39%; it does nothing for Claude.&lt;/li>
&lt;li>&lt;strong>Meter everything.&lt;/strong> Two models can finish the identical job and differ 5× on cost from verbosity alone. agentgateway makes both the swap and the measurement a one-liner.&lt;/li>
&lt;/ol>
&lt;p>Full flight, all three models, the charts, and the traces live on the flight deck: &lt;strong>&lt;a href="https://goose.maniak.ai">goose.maniak.ai&lt;/a>&lt;/strong>.&lt;/p></content:encoded></item><item><title>First Steps: agentgateway, F5 AI Guardrails, and the Enterprise UI</title><link>https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-03-agentgateway-f5-guardrails-ui-first-steps/</guid><description>&lt;p>The previous post covered the new hard spend limits in Enterprise
agentgateway v2026.6.3: model cost catalogs, dollar or token budgets, and a
real &lt;code>429&lt;/code> when a budget is exhausted. That solves the FinOps side of AI
traffic. The next question is the one security teams ask immediately after:
&lt;strong>what prevents a prompt, response, or agent workflow from leaking something
it should not?&lt;/strong>&lt;/p>
&lt;p>This is the first practical setup I use for that conversation:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>agentgateway&lt;/strong> remains the AI data plane: one OpenAI-compatible front door,
routes, backends, enterprise policies, traces, and cost/token metadata.&lt;/li>
&lt;li>&lt;strong>F5 AI Guardrails&lt;/strong> is the AI security decision point: scanners, redaction,
blocking, and audit history.&lt;/li>
&lt;li>&lt;strong>Solo Enterprise UI for agentgateway&lt;/strong> gives the platform view: routes,
destinations, policies, playground access, and traces from the gateway.&lt;/li>
&lt;/ul>
&lt;p>The goal is not just to return the right HTTP status code. The goal is to make
the setup inspectable: security sees the guardrail decision in F5, platform
sees the gateway route and policy in the agentgateway UI, and application teams
keep calling one OpenAI-compatible endpoint.&lt;/p>
&lt;h2 id="the-shape-of-the-demo">The shape of the demo&lt;/h2>
&lt;p>I deploy two F5 integration patterns side by side:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Route&lt;/th>
&lt;th>Pattern&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/option-a&lt;/code>&lt;/td>
&lt;td>agentgateway in front of F5 inline Guardrails&lt;/td>
&lt;td>agentgateway forwards to F5&amp;rsquo;s OpenAI-compatible &lt;code>/openai/{provider}/chat/completions&lt;/code> endpoint. F5 scans and makes the final provider call.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>agentgateway with out-of-band F5 ScanAPI&lt;/td>
&lt;td>agentgateway calls OpenAI directly, but request and response &lt;code>promptGuard&lt;/code> webhooks call a small adapter that sends text to F5 ScanAPI.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>I also add three native agentgateway Enterprise policy routes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Route&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/agw/direct&lt;/code>&lt;/td>
&lt;td>direct response generated by the gateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/agw/cors&lt;/code>&lt;/td>
&lt;td>CORS and response header policy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/agw/rate-limit&lt;/code>&lt;/td>
&lt;td>local rate limiting before provider/backend traffic&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That gives the demo two useful proofs at once: F5 is enforcing AI security
policy, and agentgateway Enterprise is enforcing gateway-native traffic
policy.&lt;/p>
&lt;h2 id="step-1-install-enterprise-agentgateway">Step 1: install Enterprise agentgateway&lt;/h2>
&lt;p>The demo runs on a disposable kind cluster and installs Enterprise
agentgateway &lt;code>v2026.6.3&lt;/code>.&lt;/p>
&lt;p>The important environment variables are:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">AGENTGATEWAY_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">F5_AISEC_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;https://www.us2.calypsoai.app&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">F5_AISEC_TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">F5_AISEC_INLINE_PROVIDER&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;genai-azure-openai&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">CAI_PROJECT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Global-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OPTION_A_MODEL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;gpt-4.1&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OPTION_C_MODEL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;gpt-5.5&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I keep these in &lt;code>.env&lt;/code>, which is ignored by Git. The F5 token is used in two
places: setup-time scanner creation and runtime calls from the in-cluster
adapter.&lt;/p>
&lt;p>The gateway itself is standard Kubernetes Gateway API:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything else attaches to that gateway: the F5 inline backend, the direct
OpenAI backend, the promptGuard policy, the UI tracing policy, and the native
Enterprise policy demos.&lt;/p>
&lt;h2 id="step-2-configure-f5-ai-guardrails">Step 2: configure F5 AI Guardrails&lt;/h2>
&lt;p>The setup script creates a practical scanner set in F5. I started with two
simple controls and then expanded it so the demo behaves more like an actual
security review:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scanner&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Mode&lt;/th>
&lt;th>Direction&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-codename&lt;/code>&lt;/td>
&lt;td>Keyword, &lt;code>project-titan&lt;/code>&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts and responses&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-ssn&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-email&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-phone&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-api-key&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-jwt&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-private-key&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-prompt-injection&lt;/code>&lt;/td>
&lt;td>Keyword&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-secret-exfiltration&lt;/code>&lt;/td>
&lt;td>Keyword&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-codename-obfuscation&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts and responses&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Here is that scanner set in the F5 UI:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-custom-guardrails.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-custom-guardrails.png" alt="F5 AI Guardrails custom guardrails list showing the agentgateway lab scanners for codename blocking, prompt-injection blocking, secret-exfiltration blocking, and regex redaction for private keys, JWTs, API keys, phone numbers, emails, and SSNs." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The setup script validates F5 access, resolves the project, confirms the inline
provider exists, creates or reuses the scanners, attaches them to the project,
and then runs quick ScanAPI checks:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./setup-guardrails.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For production, the direction column matters. In this demo, PII redactors are
prompt-side controls and the codename controls run both ways. If you need PII
redaction on model output as well, make those scanners &lt;code>direction: &amp;quot;both&amp;quot;&lt;/code> and
rerun the setup before you call the deployment production-ready.&lt;/p>
&lt;h2 id="step-3-option-a-f5-inline-behind-agentgateway">Step 3: Option A, F5 inline behind agentgateway&lt;/h2>
&lt;p>Option A is the fastest path because F5 already exposes an OpenAI-compatible
endpoint. agentgateway treats that endpoint like a custom OpenAI provider.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails-inline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;__OPTION_A_MODEL__&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;__F5_AISEC_HOST__&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/openai/__F5_AISEC_INLINE_PROVIDER__/chat/completions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">calypsoai-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;__F5_AISEC_HOST__&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The app calls:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST /option-a
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway receives the OpenAI Chat Completions request, applies its route
and backend policy, and forwards to F5. F5 scans the prompt and response and
owns the final provider hop.&lt;/p>
&lt;p>Use this pattern when you want a fast proof that the products work together and
the security team is comfortable owning the final provider connection in F5.&lt;/p>
&lt;h2 id="step-4-option-c-f5-scanapi-as-a-promptguard-webhook">Step 4: Option C, F5 ScanAPI as a promptGuard webhook&lt;/h2>
&lt;p>Option C keeps agentgateway as the only inference path. F5 does not proxy the
LLM request. It only renders a verdict through ScanAPI.&lt;/p>
&lt;p>The agentgateway policy targets the &lt;code>/option-c&lt;/code> HTTPRoute:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">option-c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails-adapter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FailClosed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Blocked by F5 AI Guardrails&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">statusCode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">403&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails-adapter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FailClosed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The adapter is intentionally small. It receives the webhook body, extracts the
prompt or assistant response, calls:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST /backend/v1/scans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;input&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text to scan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;project&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Global-...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scanDirection&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;request&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;flagOnly&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;verbose&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then it maps F5 outcomes back to agentgateway actions:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>F5 outcome&lt;/th>
&lt;th>Request webhook&lt;/th>
&lt;th>Response webhook&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>clear&lt;/td>
&lt;td>pass&lt;/td>
&lt;td>pass&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>blocked / flagged / rejected&lt;/td>
&lt;td>reject with &lt;code>403&lt;/code>&lt;/td>
&lt;td>mask assistant content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>redactedInput&lt;/code> returned&lt;/td>
&lt;td>replace the last user message&lt;/td>
&lt;td>replace assistant content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ScanAPI error&lt;/td>
&lt;td>fail closed with &lt;code>503&lt;/code>&lt;/td>
&lt;td>fail closed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This is the shape I prefer for production. agentgateway keeps routing,
failover, budgets, provider credentials, and traces. F5 keeps scanner policy,
redaction decisions, and audit evidence.&lt;/p>
&lt;h2 id="step-5-install-the-solo-enterprise-ui">Step 5: install the Solo Enterprise UI&lt;/h2>
&lt;p>This is the part that makes the demo much easier to explain. The UI install is
not an afterthought; it is part of the deployment.&lt;/p>
&lt;p>&lt;code>0.4.8&lt;/code> adds one prerequisite that &lt;code>0.4.7&lt;/code> did not need: the &lt;code>ui-backend&lt;/code>
container now watches &lt;code>platform.solo.io&lt;/code> CRDs (&lt;code>KubernetesCluster&lt;/code>). Install the
dedicated &lt;code>management-crds&lt;/code> chart first, or &lt;code>ui-backend&lt;/code> CrashLoopBackOffs with
&lt;code>no matches for kind &amp;quot;KubernetesCluster&amp;quot; in version &amp;quot;platform.solo.io/v1alpha1&amp;quot;&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i management-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.4.8
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then install the management chart at &lt;code>0.4.8&lt;/code> with the agentgateway product
enabled. I also turn on cost management so the UI exposes spend analytics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i management &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.4.8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">cluster&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;mgmt-cluster&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set products.agentgateway.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set products.agentgateway.features.cost-management&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string licensing.licenseKey&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">AGENTGATEWAY_LICENSE_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One subtlety worth knowing: &lt;code>products.agentgateway.features.cost-management=true&lt;/code>
renders &lt;code>PRODUCT_AGENTGATEWAY_FEATURES_COST_MANAGEMENT_ENABLED=true&lt;/code> on the
&lt;strong>&lt;code>ui-frontend&lt;/code>&lt;/strong> container — that is the flag that turns on the Cost Management
tab. The &lt;strong>&lt;code>ui-backend&lt;/code>&lt;/strong> container does not get that variable; it gets
&lt;code>AGENTGATEWAY_COST_WRITES_ENABLED&lt;/code>, driven by the separate
&lt;code>cost-management-writes&lt;/code> value (default &lt;code>true&lt;/code>). So the toggle you flip gates
the frontend UI, and a second value governs whether the backend can write
budgets, dimensions, and virtual keys.&lt;/p>
&lt;p>For a demo, I leave &lt;code>SOLO_UI_OIDC_ISSUER&lt;/code> empty so the chart&amp;rsquo;s built-in
auto-auth path is used. For a real environment, wire it to your IdP and provide
the backend client secret through a Kubernetes Secret.&lt;/p>
&lt;p>The install gives me a &lt;code>solo-enterprise-ui&lt;/code> service:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get svc -n agentgateway-system solo-enterprise-ui
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The two useful local forwards are:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/solo-enterprise-ui 8090:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then open:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">open http://localhost:8090
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-turn-on-agentgateway-traces-for-the-ui">Step 6: turn on agentgateway traces for the UI&lt;/h2>
&lt;p>The UI becomes useful for traffic analysis when agentgateway emits OTLP traces
to the management telemetry collector.&lt;/p>
&lt;p>That is a small Enterprise policy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frontend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">solo-enterprise-telemetry-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify that the policy attached:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get enterpriseagentgatewaypolicy tracing &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After traffic runs, the proxy logs include &lt;code>trace.id&lt;/code> and &lt;code>span.id&lt;/code> fields on
requests. Those are the breadcrumbs that connect gateway behavior to UI trace
views.&lt;/p>
&lt;p>This is the policy inventory in the agentgateway UI:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-policies.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-policies.png" alt="Solo Enterprise for agentgateway policies view showing active EnterpriseAgentgateway policies for CORS and headers, direct response, local rate limit, F5 guardrails, and tracing." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And this is the destination inventory. The two AI backends are exactly the two
patterns from the architecture: F5 inline and OpenAI direct.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-destinations.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-destinations.png" alt="Solo Enterprise for agentgateway destinations view showing the f5-guardrails-inline and openai-direct AI destinations in the agentgateway-system namespace." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="step-7-use-the-playground-as-a-sanity-check">Step 7: use the playground as a sanity check&lt;/h2>
&lt;p>The UI sees the routes from the cluster:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-playground-routes.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-playground-routes.png" alt="Solo Enterprise for agentgateway playground route selection showing agw-cors, agw-direct, agw-rate-limit, option-a, and option-c routes." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>That is useful in a demo because you can explain the whole deployment without
starting in YAML:&lt;/p>
&lt;ul>
&lt;li>&lt;code>/option-a&lt;/code> is the inline F5 path.&lt;/li>
&lt;li>&lt;code>/option-c&lt;/code> is the out-of-band ScanAPI path.&lt;/li>
&lt;li>&lt;code>/agw/direct&lt;/code>, &lt;code>/agw/cors&lt;/code>, and &lt;code>/agw/rate-limit&lt;/code> are native agentgateway
Enterprise policy examples.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>Routes&lt;/strong> inventory is the same set from the control plane, with match
prefix, resolved destination, gateway, and observed p95 latency once traffic has
run:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-routes.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-routes.png" alt="Solo Enterprise for agentgateway routes inventory showing agw-cors, agw-direct, agw-rate-limit, option-a, and option-c routes with match prefixes, destinations, and p95 latency." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The UI is not a replacement for &lt;code>kubectl logs&lt;/code> for raw pod stdout. Treat it as
the topology and request-visibility layer: routes, destinations, policies,
playground, and traces. Use Kubernetes logs for adapter exceptions and pod
startup messages.&lt;/p>
&lt;h2 id="step-7b-cost-management-for-the-same-traffic">Step 7b: cost management for the same traffic&lt;/h2>
&lt;p>Because I installed the UI with
&lt;code>products.agentgateway.features.cost-management=true&lt;/code>, the Cost Management tab
estimates spend for the traffic flowing through these routes. Spend is computed
from token counts × your configured per-token prices — an estimate, not the
provider&amp;rsquo;s invoice — and it breaks down by provider, model family, model,
group, user, and virtual key.&lt;/p>
&lt;!-- TODO: add Cost Management dashboard screenshot at /images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-cost-management.png -->
&lt;p>This is where budgets and guardrails meet in one console: the same route that F5
scans for safety also reports what it costs. Pair it with
&lt;code>EnterpriseAgentgatewayBudget&lt;/code> (from the hard spend limits article) to enforce a
ceiling, not just observe one.&lt;/p>
&lt;h2 id="step-8-prove-enforcement-with-traffic">Step 8: prove enforcement with traffic&lt;/h2>
&lt;p>The smoke test sends six OpenAI Chat Completions requests:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./test.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The passing run:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/smoke-test-pass.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/smoke-test-pass.png" alt="Terminal output showing the agentgateway plus F5 smoke test passing: benign requests return 200, codename blocks return 400 or 403, SSN redaction succeeds, and response-phase scanning masks blocked output." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Text version:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">PASS Option A benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option A blocked codename: HTTP 400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C blocked codename: HTTP 403
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C SSN redaction request completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C redaction did not leak raw SSN
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scan completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scanner masked blocked output
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That proves the data plane behavior:&lt;/p>
&lt;ul>
&lt;li>benign traffic reaches the model&lt;/li>
&lt;li>&lt;code>project-titan&lt;/code> is blocked&lt;/li>
&lt;li>SSN-shaped prompt content is redacted before forwarding&lt;/li>
&lt;li>response content that trips the response scanner is masked before the client
sees it&lt;/li>
&lt;/ul>
&lt;h2 id="step-9-confirm-the-f5-audit-trail">Step 9: confirm the F5 audit trail&lt;/h2>
&lt;p>The same traffic shows up in F5 under &lt;strong>Logs -&amp;gt; Prompt history&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-prompt-history.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-prompt-history.png" alt="F5 AI Guardrails prompt history showing blocked project-titan scans, redacted SSN scans, and the inline Genai Azure Openai prompt path from the agentgateway demo." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>This is the evidence split I want in the operating model:&lt;/p>
&lt;ul>
&lt;li>agentgateway proves which route, policy, backend, status code, model, token
count, cost, trace ID, and latency were involved.&lt;/li>
&lt;li>F5 proves which scanner matched, whether content was blocked or redacted,
who initiated it, and when the decision happened.&lt;/li>
&lt;/ul>
&lt;p>Those are different audit questions. Do not force one product to answer both.&lt;/p>
&lt;h2 id="the-deployment-checklist">The deployment checklist&lt;/h2>
&lt;p>For a fresh lab, the sequence is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cp .env.example .env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># fill in AGENTGATEWAY_LICENSE_KEY, OPENAI_API_KEY, F5_AISEC_URL, F5_AISEC_TOKEN&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup-guardrails.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/solo-enterprise-ui 8090:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test_agentgateway.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">HARNESS_CASES&lt;/span>&lt;span class="o">=&lt;/span>harness/intense-cases.yaml ./run_harness.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The checks I care about before showing this to anyone:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm list -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods,svc -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get enterpriseagentgatewaypolicy -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system deploy/agentgateway-proxy --tail&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system deploy/f5-guardrails-adapter --tail&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The healthy state should include:&lt;/p>
&lt;ul>
&lt;li>&lt;code>enterprise-agentgateway&lt;/code> chart at &lt;code>v2026.6.3&lt;/code>&lt;/li>
&lt;li>&lt;code>management-crds&lt;/code> chart installed (so &lt;code>ui-backend&lt;/code> finds the &lt;code>platform.solo.io&lt;/code>
CRDs)&lt;/li>
&lt;li>&lt;code>management&lt;/code> chart at &lt;code>0.4.8&lt;/code>&lt;/li>
&lt;li>&lt;code>solo-enterprise-ui&lt;/code> pod &lt;code>5/5&lt;/code> Ready (a crash-looping &lt;code>ui-backend&lt;/code> means the
&lt;code>management-crds&lt;/code> install was skipped)&lt;/li>
&lt;li>&lt;code>solo-enterprise-telemetry-collector&lt;/code> running&lt;/li>
&lt;li>&lt;code>tracing&lt;/code> policy accepted and attached&lt;/li>
&lt;li>Cost Management tab visible in the UI
(&lt;code>PRODUCT_AGENTGATEWAY_FEATURES_COST_MANAGEMENT_ENABLED=true&lt;/code> on &lt;code>ui-frontend&lt;/code>)&lt;/li>
&lt;li>agentgateway request logs with &lt;code>trace.id&lt;/code> and &lt;code>span.id&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="what-this-adds-on-top-of-hard-spend-limits">What this adds on top of hard spend limits&lt;/h2>
&lt;p>The budget article showed that agentgateway can stop runaway cost at the
gateway. This setup adds the security controls around the same traffic:&lt;/p>
&lt;ul>
&lt;li>budgets answer &lt;strong>how much can this team spend?&lt;/strong>&lt;/li>
&lt;li>guardrails answer &lt;strong>is this prompt or response allowed?&lt;/strong>&lt;/li>
&lt;li>routes and policies answer &lt;strong>where is this traffic allowed to go?&lt;/strong>&lt;/li>
&lt;li>traces answer &lt;strong>what happened on this request?&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>The key is that these controls are attached to the gateway, not hand-coded in
every application. Apps keep using normal OpenAI Chat Completions calls.
Platform and security teams govern the path.&lt;/p>
&lt;h2 id="what-i-would-harden-next">What I would harden next&lt;/h2>
&lt;p>For a production-grade rollout, I would tighten four things:&lt;/p>
&lt;ol>
&lt;li>Change PII redactors that must protect model output to &lt;code>direction: &amp;quot;both&amp;quot;&lt;/code>.&lt;/li>
&lt;li>Put the adapter behind real service-level observability and alert on
fail-closed &lt;code>503&lt;/code>s.&lt;/li>
&lt;li>Wire the Enterprise UI to the corporate IdP instead of demo auto-auth.&lt;/li>
&lt;li>Combine this with &lt;code>EnterpriseAgentgatewayBudget&lt;/code> so unsafe traffic and
runaway spend are both blocked at the same front door.&lt;/li>
&lt;/ol>
&lt;p>That is the platform story: one gateway, separate controls, clear ownership,
and enough visibility that you can prove what happened after the fact.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">Hard Spend Limits for LLM Traffic: AI Budgets in Enterprise agentgateway v2026.6.3&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/">Three Ways to Combine agentgateway with F5 AI Guardrails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai">Runnable lab: agentgateway + F5 AI Guardrails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/latest/">Solo Enterprise for agentgateway docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.aisecurity.f5.com/">F5 AI Guardrails docs&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>The previous post covered the new hard spend limits in Enterprise
agentgateway v2026.6.3: model cost catalogs, dollar or token budgets, and a
real &lt;code>429&lt;/code> when a budget is exhausted. That solves the FinOps side of AI
traffic. The next question is the one security teams ask immediately after:
&lt;strong>what prevents a prompt, response, or agent workflow from leaking something
it should not?&lt;/strong>&lt;/p>
&lt;p>This is the first practical setup I use for that conversation:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>agentgateway&lt;/strong> remains the AI data plane: one OpenAI-compatible front door,
routes, backends, enterprise policies, traces, and cost/token metadata.&lt;/li>
&lt;li>&lt;strong>F5 AI Guardrails&lt;/strong> is the AI security decision point: scanners, redaction,
blocking, and audit history.&lt;/li>
&lt;li>&lt;strong>Solo Enterprise UI for agentgateway&lt;/strong> gives the platform view: routes,
destinations, policies, playground access, and traces from the gateway.&lt;/li>
&lt;/ul>
&lt;p>The goal is not just to return the right HTTP status code. The goal is to make
the setup inspectable: security sees the guardrail decision in F5, platform
sees the gateway route and policy in the agentgateway UI, and application teams
keep calling one OpenAI-compatible endpoint.&lt;/p>
&lt;h2 id="the-shape-of-the-demo">The shape of the demo&lt;/h2>
&lt;p>I deploy two F5 integration patterns side by side:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Route&lt;/th>
&lt;th>Pattern&lt;/th>
&lt;th>What happens&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/option-a&lt;/code>&lt;/td>
&lt;td>agentgateway in front of F5 inline Guardrails&lt;/td>
&lt;td>agentgateway forwards to F5&amp;rsquo;s OpenAI-compatible &lt;code>/openai/{provider}/chat/completions&lt;/code> endpoint. F5 scans and makes the final provider call.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>agentgateway with out-of-band F5 ScanAPI&lt;/td>
&lt;td>agentgateway calls OpenAI directly, but request and response &lt;code>promptGuard&lt;/code> webhooks call a small adapter that sends text to F5 ScanAPI.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>I also add three native agentgateway Enterprise policy routes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Route&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/agw/direct&lt;/code>&lt;/td>
&lt;td>direct response generated by the gateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/agw/cors&lt;/code>&lt;/td>
&lt;td>CORS and response header policy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/agw/rate-limit&lt;/code>&lt;/td>
&lt;td>local rate limiting before provider/backend traffic&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That gives the demo two useful proofs at once: F5 is enforcing AI security
policy, and agentgateway Enterprise is enforcing gateway-native traffic
policy.&lt;/p>
&lt;h2 id="step-1-install-enterprise-agentgateway">Step 1: install Enterprise agentgateway&lt;/h2>
&lt;p>The demo runs on a disposable kind cluster and installs Enterprise
agentgateway &lt;code>v2026.6.3&lt;/code>.&lt;/p>
&lt;p>The important environment variables are:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">AGENTGATEWAY_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">F5_AISEC_URL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;https://www.us2.calypsoai.app&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">F5_AISEC_TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">F5_AISEC_INLINE_PROVIDER&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;genai-azure-openai&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">CAI_PROJECT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;Global-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OPTION_A_MODEL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;gpt-4.1&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">OPTION_C_MODEL&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;gpt-5.5&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I keep these in &lt;code>.env&lt;/code>, which is ignored by Git. The F5 token is used in two
places: setup-time scanner creation and runtime calls from the in-cluster
adapter.&lt;/p>
&lt;p>The gateway itself is standard Kubernetes Gateway API:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything else attaches to that gateway: the F5 inline backend, the direct
OpenAI backend, the promptGuard policy, the UI tracing policy, and the native
Enterprise policy demos.&lt;/p>
&lt;h2 id="step-2-configure-f5-ai-guardrails">Step 2: configure F5 AI Guardrails&lt;/h2>
&lt;p>The setup script creates a practical scanner set in F5. I started with two
simple controls and then expanded it so the demo behaves more like an actual
security review:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scanner&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Mode&lt;/th>
&lt;th>Direction&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-codename&lt;/code>&lt;/td>
&lt;td>Keyword, &lt;code>project-titan&lt;/code>&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts and responses&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-ssn&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-email&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-phone&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-api-key&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-jwt&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-private-key&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Redact&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-prompt-injection&lt;/code>&lt;/td>
&lt;td>Keyword&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-secret-exfiltration&lt;/code>&lt;/td>
&lt;td>Keyword&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-codename-obfuscation&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>Block&lt;/td>
&lt;td>Prompts and responses&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Here is that scanner set in the F5 UI:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-custom-guardrails.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-custom-guardrails.png" alt="F5 AI Guardrails custom guardrails list showing the agentgateway lab scanners for codename blocking, prompt-injection blocking, secret-exfiltration blocking, and regex redaction for private keys, JWTs, API keys, phone numbers, emails, and SSNs." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The setup script validates F5 access, resolves the project, confirms the inline
provider exists, creates or reuses the scanners, attaches them to the project,
and then runs quick ScanAPI checks:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./setup-guardrails.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For production, the direction column matters. In this demo, PII redactors are
prompt-side controls and the codename controls run both ways. If you need PII
redaction on model output as well, make those scanners &lt;code>direction: &amp;quot;both&amp;quot;&lt;/code> and
rerun the setup before you call the deployment production-ready.&lt;/p>
&lt;h2 id="step-3-option-a-f5-inline-behind-agentgateway">Step 3: Option A, F5 inline behind agentgateway&lt;/h2>
&lt;p>Option A is the fastest path because F5 already exposes an OpenAI-compatible
endpoint. agentgateway treats that endpoint like a custom OpenAI provider.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails-inline&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;__OPTION_A_MODEL__&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;__F5_AISEC_HOST__&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/openai/__F5_AISEC_INLINE_PROVIDER__/chat/completions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">calypsoai-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;__F5_AISEC_HOST__&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The app calls:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST /option-a
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway receives the OpenAI Chat Completions request, applies its route
and backend policy, and forwards to F5. F5 scans the prompt and response and
owns the final provider hop.&lt;/p>
&lt;p>Use this pattern when you want a fast proof that the products work together and
the security team is comfortable owning the final provider connection in F5.&lt;/p>
&lt;h2 id="step-4-option-c-f5-scanapi-as-a-promptguard-webhook">Step 4: Option C, F5 ScanAPI as a promptGuard webhook&lt;/h2>
&lt;p>Option C keeps agentgateway as the only inference path. F5 does not proxy the
LLM request. It only renders a verdict through ScanAPI.&lt;/p>
&lt;p>The agentgateway policy targets the &lt;code>/option-c&lt;/code> HTTPRoute:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">option-c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails-adapter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FailClosed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">message&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Blocked by F5 AI Guardrails&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">statusCode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">403&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails-adapter&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FailClosed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The adapter is intentionally small. It receives the webhook body, extracts the
prompt or assistant response, calls:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">POST /backend/v1/scans
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;input&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text to scan&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;project&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Global-...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scanDirection&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;request&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;flagOnly&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;verbose&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then it maps F5 outcomes back to agentgateway actions:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>F5 outcome&lt;/th>
&lt;th>Request webhook&lt;/th>
&lt;th>Response webhook&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>clear&lt;/td>
&lt;td>pass&lt;/td>
&lt;td>pass&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>blocked / flagged / rejected&lt;/td>
&lt;td>reject with &lt;code>403&lt;/code>&lt;/td>
&lt;td>mask assistant content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>redactedInput&lt;/code> returned&lt;/td>
&lt;td>replace the last user message&lt;/td>
&lt;td>replace assistant content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ScanAPI error&lt;/td>
&lt;td>fail closed with &lt;code>503&lt;/code>&lt;/td>
&lt;td>fail closed&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This is the shape I prefer for production. agentgateway keeps routing,
failover, budgets, provider credentials, and traces. F5 keeps scanner policy,
redaction decisions, and audit evidence.&lt;/p>
&lt;h2 id="step-5-install-the-solo-enterprise-ui">Step 5: install the Solo Enterprise UI&lt;/h2>
&lt;p>This is the part that makes the demo much easier to explain. The UI install is
not an afterthought; it is part of the deployment.&lt;/p>
&lt;p>&lt;code>0.4.8&lt;/code> adds one prerequisite that &lt;code>0.4.7&lt;/code> did not need: the &lt;code>ui-backend&lt;/code>
container now watches &lt;code>platform.solo.io&lt;/code> CRDs (&lt;code>KubernetesCluster&lt;/code>). Install the
dedicated &lt;code>management-crds&lt;/code> chart first, or &lt;code>ui-backend&lt;/code> CrashLoopBackOffs with
&lt;code>no matches for kind &amp;quot;KubernetesCluster&amp;quot; in version &amp;quot;platform.solo.io/v1alpha1&amp;quot;&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i management-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.4.8
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then install the management chart at &lt;code>0.4.8&lt;/code> with the agentgateway product
enabled. I also turn on cost management so the UI exposes spend analytics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i management &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/solo-enterprise-helm/charts/management &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.4.8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">cluster&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;mgmt-cluster&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set products.agentgateway.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set products.agentgateway.features.cost-management&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string licensing.licenseKey&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">AGENTGATEWAY_LICENSE_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One subtlety worth knowing: &lt;code>products.agentgateway.features.cost-management=true&lt;/code>
renders &lt;code>PRODUCT_AGENTGATEWAY_FEATURES_COST_MANAGEMENT_ENABLED=true&lt;/code> on the
&lt;strong>&lt;code>ui-frontend&lt;/code>&lt;/strong> container — that is the flag that turns on the Cost Management
tab. The &lt;strong>&lt;code>ui-backend&lt;/code>&lt;/strong> container does not get that variable; it gets
&lt;code>AGENTGATEWAY_COST_WRITES_ENABLED&lt;/code>, driven by the separate
&lt;code>cost-management-writes&lt;/code> value (default &lt;code>true&lt;/code>). So the toggle you flip gates
the frontend UI, and a second value governs whether the backend can write
budgets, dimensions, and virtual keys.&lt;/p>
&lt;p>For a demo, I leave &lt;code>SOLO_UI_OIDC_ISSUER&lt;/code> empty so the chart&amp;rsquo;s built-in
auto-auth path is used. For a real environment, wire it to your IdP and provide
the backend client secret through a Kubernetes Secret.&lt;/p>
&lt;p>The install gives me a &lt;code>solo-enterprise-ui&lt;/code> service:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get svc -n agentgateway-system solo-enterprise-ui
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The two useful local forwards are:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/solo-enterprise-ui 8090:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then open:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">open http://localhost:8090
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-turn-on-agentgateway-traces-for-the-ui">Step 6: turn on agentgateway traces for the UI&lt;/h2>
&lt;p>The UI becomes useful for traffic analysis when agentgateway emits OTLP traces
to the management telemetry collector.&lt;/p>
&lt;p>That is a small Enterprise policy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frontend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">solo-enterprise-telemetry-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify that the policy attached:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get enterpriseagentgatewaypolicy tracing &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After traffic runs, the proxy logs include &lt;code>trace.id&lt;/code> and &lt;code>span.id&lt;/code> fields on
requests. Those are the breadcrumbs that connect gateway behavior to UI trace
views.&lt;/p>
&lt;p>This is the policy inventory in the agentgateway UI:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-policies.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-policies.png" alt="Solo Enterprise for agentgateway policies view showing active EnterpriseAgentgateway policies for CORS and headers, direct response, local rate limit, F5 guardrails, and tracing." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And this is the destination inventory. The two AI backends are exactly the two
patterns from the architecture: F5 inline and OpenAI direct.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-destinations.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-destinations.png" alt="Solo Enterprise for agentgateway destinations view showing the f5-guardrails-inline and openai-direct AI destinations in the agentgateway-system namespace." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="step-7-use-the-playground-as-a-sanity-check">Step 7: use the playground as a sanity check&lt;/h2>
&lt;p>The UI sees the routes from the cluster:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-playground-routes.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-playground-routes.png" alt="Solo Enterprise for agentgateway playground route selection showing agw-cors, agw-direct, agw-rate-limit, option-a, and option-c routes." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>That is useful in a demo because you can explain the whole deployment without
starting in YAML:&lt;/p>
&lt;ul>
&lt;li>&lt;code>/option-a&lt;/code> is the inline F5 path.&lt;/li>
&lt;li>&lt;code>/option-c&lt;/code> is the out-of-band ScanAPI path.&lt;/li>
&lt;li>&lt;code>/agw/direct&lt;/code>, &lt;code>/agw/cors&lt;/code>, and &lt;code>/agw/rate-limit&lt;/code> are native agentgateway
Enterprise policy examples.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>Routes&lt;/strong> inventory is the same set from the control plane, with match
prefix, resolved destination, gateway, and observed p95 latency once traffic has
run:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-routes.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-routes.png" alt="Solo Enterprise for agentgateway routes inventory showing agw-cors, agw-direct, agw-rate-limit, option-a, and option-c routes with match prefixes, destinations, and p95 latency." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The UI is not a replacement for &lt;code>kubectl logs&lt;/code> for raw pod stdout. Treat it as
the topology and request-visibility layer: routes, destinations, policies,
playground, and traces. Use Kubernetes logs for adapter exceptions and pod
startup messages.&lt;/p>
&lt;h2 id="step-7b-cost-management-for-the-same-traffic">Step 7b: cost management for the same traffic&lt;/h2>
&lt;p>Because I installed the UI with
&lt;code>products.agentgateway.features.cost-management=true&lt;/code>, the Cost Management tab
estimates spend for the traffic flowing through these routes. Spend is computed
from token counts × your configured per-token prices — an estimate, not the
provider&amp;rsquo;s invoice — and it breaks down by provider, model family, model,
group, user, and virtual key.&lt;/p>
&lt;!-- TODO: add Cost Management dashboard screenshot at /images/articles/2026-07-03-agentgateway-f5-ui-setup/agentgateway-cost-management.png -->
&lt;p>This is where budgets and guardrails meet in one console: the same route that F5
scans for safety also reports what it costs. Pair it with
&lt;code>EnterpriseAgentgatewayBudget&lt;/code> (from the hard spend limits article) to enforce a
ceiling, not just observe one.&lt;/p>
&lt;h2 id="step-8-prove-enforcement-with-traffic">Step 8: prove enforcement with traffic&lt;/h2>
&lt;p>The smoke test sends six OpenAI Chat Completions requests:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./test.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The passing run:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/smoke-test-pass.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/smoke-test-pass.png" alt="Terminal output showing the agentgateway plus F5 smoke test passing: benign requests return 200, codename blocks return 400 or 403, SSN redaction succeeds, and response-phase scanning masks blocked output." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Text version:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">PASS Option A benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option A blocked codename: HTTP 400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C blocked codename: HTTP 403
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C SSN redaction request completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C redaction did not leak raw SSN
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scan completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scanner masked blocked output
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That proves the data plane behavior:&lt;/p>
&lt;ul>
&lt;li>benign traffic reaches the model&lt;/li>
&lt;li>&lt;code>project-titan&lt;/code> is blocked&lt;/li>
&lt;li>SSN-shaped prompt content is redacted before forwarding&lt;/li>
&lt;li>response content that trips the response scanner is masked before the client
sees it&lt;/li>
&lt;/ul>
&lt;h2 id="step-9-confirm-the-f5-audit-trail">Step 9: confirm the F5 audit trail&lt;/h2>
&lt;p>The same traffic shows up in F5 under &lt;strong>Logs -&amp;gt; Prompt history&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-prompt-history.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-03-agentgateway-f5-ui-setup/f5-prompt-history.png" alt="F5 AI Guardrails prompt history showing blocked project-titan scans, redacted SSN scans, and the inline Genai Azure Openai prompt path from the agentgateway demo." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>This is the evidence split I want in the operating model:&lt;/p>
&lt;ul>
&lt;li>agentgateway proves which route, policy, backend, status code, model, token
count, cost, trace ID, and latency were involved.&lt;/li>
&lt;li>F5 proves which scanner matched, whether content was blocked or redacted,
who initiated it, and when the decision happened.&lt;/li>
&lt;/ul>
&lt;p>Those are different audit questions. Do not force one product to answer both.&lt;/p>
&lt;h2 id="the-deployment-checklist">The deployment checklist&lt;/h2>
&lt;p>For a fresh lab, the sequence is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cp .env.example .env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># fill in AGENTGATEWAY_LICENSE_KEY, OPENAI_API_KEY, F5_AISEC_URL, F5_AISEC_TOKEN&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup-guardrails.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/solo-enterprise-ui 8090:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test_agentgateway.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">HARNESS_CASES&lt;/span>&lt;span class="o">=&lt;/span>harness/intense-cases.yaml ./run_harness.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The checks I care about before showing this to anyone:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm list -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods,svc -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get enterpriseagentgatewaypolicy -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system deploy/agentgateway-proxy --tail&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system deploy/f5-guardrails-adapter --tail&lt;span class="o">=&lt;/span>&lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The healthy state should include:&lt;/p>
&lt;ul>
&lt;li>&lt;code>enterprise-agentgateway&lt;/code> chart at &lt;code>v2026.6.3&lt;/code>&lt;/li>
&lt;li>&lt;code>management-crds&lt;/code> chart installed (so &lt;code>ui-backend&lt;/code> finds the &lt;code>platform.solo.io&lt;/code>
CRDs)&lt;/li>
&lt;li>&lt;code>management&lt;/code> chart at &lt;code>0.4.8&lt;/code>&lt;/li>
&lt;li>&lt;code>solo-enterprise-ui&lt;/code> pod &lt;code>5/5&lt;/code> Ready (a crash-looping &lt;code>ui-backend&lt;/code> means the
&lt;code>management-crds&lt;/code> install was skipped)&lt;/li>
&lt;li>&lt;code>solo-enterprise-telemetry-collector&lt;/code> running&lt;/li>
&lt;li>&lt;code>tracing&lt;/code> policy accepted and attached&lt;/li>
&lt;li>Cost Management tab visible in the UI
(&lt;code>PRODUCT_AGENTGATEWAY_FEATURES_COST_MANAGEMENT_ENABLED=true&lt;/code> on &lt;code>ui-frontend&lt;/code>)&lt;/li>
&lt;li>agentgateway request logs with &lt;code>trace.id&lt;/code> and &lt;code>span.id&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="what-this-adds-on-top-of-hard-spend-limits">What this adds on top of hard spend limits&lt;/h2>
&lt;p>The budget article showed that agentgateway can stop runaway cost at the
gateway. This setup adds the security controls around the same traffic:&lt;/p>
&lt;ul>
&lt;li>budgets answer &lt;strong>how much can this team spend?&lt;/strong>&lt;/li>
&lt;li>guardrails answer &lt;strong>is this prompt or response allowed?&lt;/strong>&lt;/li>
&lt;li>routes and policies answer &lt;strong>where is this traffic allowed to go?&lt;/strong>&lt;/li>
&lt;li>traces answer &lt;strong>what happened on this request?&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>The key is that these controls are attached to the gateway, not hand-coded in
every application. Apps keep using normal OpenAI Chat Completions calls.
Platform and security teams govern the path.&lt;/p>
&lt;h2 id="what-i-would-harden-next">What I would harden next&lt;/h2>
&lt;p>For a production-grade rollout, I would tighten four things:&lt;/p>
&lt;ol>
&lt;li>Change PII redactors that must protect model output to &lt;code>direction: &amp;quot;both&amp;quot;&lt;/code>.&lt;/li>
&lt;li>Put the adapter behind real service-level observability and alert on
fail-closed &lt;code>503&lt;/code>s.&lt;/li>
&lt;li>Wire the Enterprise UI to the corporate IdP instead of demo auto-auth.&lt;/li>
&lt;li>Combine this with &lt;code>EnterpriseAgentgatewayBudget&lt;/code> so unsafe traffic and
runaway spend are both blocked at the same front door.&lt;/li>
&lt;/ol>
&lt;p>That is the platform story: one gateway, separate controls, clear ownership,
and enough visibility that you can prove what happened after the fact.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/">Hard Spend Limits for LLM Traffic: AI Budgets in Enterprise agentgateway v2026.6.3&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://maniak.io/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/">Three Ways to Combine agentgateway with F5 AI Guardrails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai">Runnable lab: agentgateway + F5 AI Guardrails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/latest/">Solo Enterprise for agentgateway docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.aisecurity.f5.com/">F5 AI Guardrails docs&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Hard Spend Limits for LLM Traffic: AI Budgets in Enterprise AgentGateway v2026.6.3</title><link>https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-02-agentgateway-ai-budgets-hard-spend-limits/</guid><description>&lt;p>Enterprise AgentGateway v2026.6.3 shipped on July 1st with a changelog line that
FinOps-minded platform teams have been waiting for: &lt;strong>Enterprise Budgets and
Dimensions&lt;/strong>. Until now, &amp;ldquo;budgeting&amp;rdquo; LLM traffic through a gateway meant
approximating it with token-based rate limiting — workable, but request-window
based, tokens-only, and fiddly to reason about. The new release replaces that
with a first-class Kubernetes resource: &lt;code>EnterpriseAgentgatewayBudget&lt;/code>, a
declarative spending limit in &lt;strong>US dollars or tokens&lt;/strong>, accumulated over a
&lt;strong>rolling window&lt;/strong> (day, week, month, year), with a choice of &lt;strong>audit&lt;/strong> or
&lt;strong>block&lt;/strong> when the budget runs dry.&lt;/p>
&lt;p>This post is a day-one field report. The docs for the feature are still
catching up with the release (only the API reference covers it as I write
this), so everything below was derived from the live CRD schemas on my cluster
and verified with real traffic — including tripping a budget on purpose and
watching the gateway return &lt;code>429&lt;/code> with the window in the reset header.&lt;/p>
&lt;h2 id="why-budgets-not-rate-limits">Why budgets, not rate limits&lt;/h2>
&lt;p>Rate limits answer &amp;ldquo;how fast?&amp;rdquo; Budgets answer &amp;ldquo;how much, in dollars, this
month?&amp;rdquo; — which is the question your finance team actually asks. The
difference shows up in three places:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Unit.&lt;/strong> A budget is denominated in &lt;code>USD&lt;/code> or &lt;code>Tokens&lt;/code>. USD budgets use a
&lt;em>model cost catalog&lt;/em> (per-model input/output rates) so the gateway computes
the realized cost of every request as it happens.&lt;/li>
&lt;li>&lt;strong>Window.&lt;/strong> Budgets accumulate over &lt;strong>rolling windows&lt;/strong> — &lt;code>Day&lt;/code>, &lt;code>Week&lt;/code>,
&lt;code>Month&lt;/code>, &lt;code>Year&lt;/code>. Under the hood, enforcement rides the gateway&amp;rsquo;s rate
limiting infrastructure, so spend ages out continuously rather than
resetting at a calendar boundary: a &lt;code>Day&lt;/code> budget looks at the trailing 24
hours, not &amp;ldquo;since midnight.&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>Action.&lt;/strong> &lt;code>onBudgetExceeded: Audit&lt;/code> logs and lets traffic through (perfect
for rollout); &lt;code>Block&lt;/code> returns &lt;code>429&lt;/code> until enough spend rolls off the window
to bring you back under the limit. You can run both at once: a generous
audited dollar budget for visibility, plus a hard token stop as a circuit
breaker.&lt;/li>
&lt;/ul>
&lt;h2 id="the-moving-parts">The moving parts&lt;/h2>
&lt;p>Three resources cooperate, all in the &lt;code>enterpriseagentgateway.solo.io/v1alpha1&lt;/code>
API group:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>EnterpriseAgentgatewayBudget&lt;/code>&lt;/strong> — the budget definitions themselves. Each
entry has a &lt;code>name&lt;/code>, a &lt;code>limit&lt;/code> (&lt;code>unit&lt;/code> + &lt;code>amount&lt;/code>), a &lt;code>window&lt;/code>, an
&lt;code>onBudgetExceeded&lt;/code> action, and an optional &lt;code>subject&lt;/code> — up to &lt;strong>16 key/value
dimensions&lt;/strong> that scope the entry to matching requests (per user, team, API
key, model… &lt;code>&amp;quot;*&amp;quot;&lt;/code> matches any non-missing value; omit &lt;code>subject&lt;/code> and the
budget applies to every request on the enforced target).&lt;/li>
&lt;li>&lt;strong>&lt;code>EnterpriseAgentgatewayPolicy&lt;/code>&lt;/strong> with &lt;code>traffic.entBudgetEnforcement&lt;/code> —
switches enforcement on for the gateways or routes in &lt;code>targetRefs&lt;/code>, and
controls &lt;em>discovery&lt;/em>: which namespaces the controller scans for Budget
resources (&lt;code>Same&lt;/code>, &lt;code>Selector&lt;/code>, or &lt;code>All&lt;/code> — handy for letting each team keep
its own Budget objects in its own namespace).&lt;/li>
&lt;li>&lt;strong>&lt;code>EnterpriseAgentgatewayParameters&lt;/code>&lt;/strong> with &lt;code>modelCatalog&lt;/code> — loads per-model
pricing from a ConfigMap so USD budgets (and per-request cost telemetry)
have something to compute with.&lt;/li>
&lt;/ol>
&lt;h2 id="the-lab">The lab&lt;/h2>
&lt;p>My setup is a bare-metal Talos Kubernetes cluster running Solo Enterprise
AgentGateway v2026.6.3, fully GitOps-managed by ArgoCD — every manifest below
lives in a repo and lands via auto-sync. The main gateway (&lt;code>agentgateway-proxy&lt;/code>)
already fronts OpenAI at &lt;code>/openai&lt;/code>.&lt;/p>
&lt;p>One design note worth stealing: my &lt;code>/openai&lt;/code> route is also the default model
path for kagent agents in the same cluster. A blocking budget that trips would
cut those agents off for up to 24 hours. So the demo gets a &lt;strong>dedicated
route&lt;/strong> — same backend, different path — and the budget policy targets only
that route. Blast radius: zero.&lt;/p>
&lt;h2 id="step-1--the-model-cost-catalog">Step 1 — the model cost catalog&lt;/h2>
&lt;p>Per-model USD rates, per &lt;strong>1M tokens&lt;/strong>, as a ConfigMap:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">model-cost-catalog&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">catalog.json&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;providers&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;openai&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;models&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;gpt-4o&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;rates&amp;#34;: { &amp;#34;input&amp;#34;: &amp;#34;2.50&amp;#34;, &amp;#34;output&amp;#34;: &amp;#34;10.00&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;gpt-4o-mini&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;rates&amp;#34;: { &amp;#34;input&amp;#34;: &amp;#34;0.15&amp;#34;, &amp;#34;output&amp;#34;: &amp;#34;0.60&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Treat pricing as versioned configuration — when a provider changes rates, you
change a file in Git and your cost telemetry follows.&lt;/p>
&lt;h2 id="step-2--load-the-catalog-into-the-proxy">Step 2 — load the catalog into the proxy&lt;/h2>
&lt;p>The catalog attaches to a gateway through &lt;code>EnterpriseAgentgatewayParameters&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy-params&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">model-cost-catalog&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">catalog.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>…and the Gateway references it via &lt;code>infrastructure.parametersRef&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">infrastructure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parametersRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy-params&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Heads-up: attaching parameters rolls the proxy Deployment (a new pod came up in
seconds in my lab). On startup the proxy confirms the catalog load:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">info&lt;/span> &lt;span class="n">llm&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="n">cost&lt;/span> &lt;span class="n">watching&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">catalog&lt;/span> &lt;span class="n">files&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">info&lt;/span> &lt;span class="n">llm&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="n">cost&lt;/span> &lt;span class="n">loaded&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">catalog&lt;/span> &lt;span class="n">providers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span> &lt;span class="n">models&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">80&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note &lt;code>providers=3 models=80&lt;/code> — your catalog merges with a built-in one, so you
only need to define models where you want to pin your own rates.&lt;/p>
&lt;h2 id="step-3--an-isolated-demo-route">Step 3 — an isolated demo route&lt;/h2>
&lt;p>Same OpenAI backend, dedicated path:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4--the-budgets-and-their-enforcement">Step 4 — the budgets and their enforcement&lt;/h2>
&lt;p>Two entries: a $5/day audited dollar budget (visibility) and a deliberately
tiny 2,000-token/day blocking budget (the circuit breaker we&amp;rsquo;re going to trip):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayBudget&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budgets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">demo-usd-audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">demo-token-block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo-enforcement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traffic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entBudgetEnforcement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Same&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Push, let ArgoCD sync, and check the policy attached:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ kubectl -n agentgateway-system get enterpriseagentgatewaypolicy budget-demo-enforcement
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME ACCEPTED ATTACHED AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">budget-demo-enforcement True True 29s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="tripping-it">Tripping it&lt;/h2>
&lt;p>First request — normal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ curl -s http://&amp;lt;node-ip&amp;gt;:30160/budget-demo/v1/chat/completions \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -H &amp;#39;Content-Type: application/json&amp;#39; \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -d &amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;gpt-4o&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;In one sentence, what is a token budget?&amp;#34;}],&amp;#34;max_tokens&amp;#34;:60}&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">HTTP/1.1 200 OK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;usage&amp;#34;:{&amp;#34;prompt_tokens&amp;#34;:17,&amp;#34;completion_tokens&amp;#34;:39,&amp;#34;total_tokens&amp;#34;:56}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then a few large completions (&lt;code>max_tokens: 800&lt;/code>, a 500-word essay each) to burn
the 2,000-token daily budget:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">request 1: HTTP/1.1 200 OK &amp;#34;total_tokens&amp;#34;:704
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">request 2: HTTP/1.1 200 OK &amp;#34;total_tokens&amp;#34;:711
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">request 3: HTTP/1.1 200 OK &amp;#34;total_tokens&amp;#34;:669
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">request 4: HTTP/1.1 429 Too Many Requests
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>56 + 704 + 711 + 669 = 2,140 tokens — over budget. Request 4 never reaches
OpenAI. The &lt;code>429&lt;/code> response is admission-level and self-describing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">HTTP/1.1 429 Too Many Requests
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">x-ratelimit-limit: 2000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">x-ratelimit-remaining: 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">x-ratelimit-reset: 86400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rate limit exceeded
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>x-ratelimit-reset: 86400&lt;/code> — the 24-hour rolling window. Clients get exactly
what they need to back off intelligently.&lt;/p>
&lt;p>Meanwhile the production &lt;code>/openai&lt;/code> route (which kagent&amp;rsquo;s agents use) kept
returning &lt;code>200&lt;/code> throughout — the budget is scoped to the demo route and
nothing else.&lt;/p>
&lt;h2 id="what-you-get-in-the-logs">What you get in the logs&lt;/h2>
&lt;p>This is my favorite part. With the catalog loaded, &lt;strong>every LLM request logs
its realized dollar cost&lt;/strong> alongside the usual token telemetry:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">info request route=agentgateway-system/budget-demo ... http.status=200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gen_ai.request.model=gpt-4o gen_ai.response.model=gpt-4o-2024-08-06
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gen_ai.usage.input_tokens=21 gen_ai.usage.output_tokens=683
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agw.ai.usage.cost.total=0.0068825
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s $0.0069 for a 704-token gpt-4o call, computed from the catalog at
request time — the same number your USD budgets accrue against, and it flows
into traces and metrics for your observability stack.&lt;/p>
&lt;p>And when the blocking budget trips, the gateway says so in plain terms:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">warn budget budget exceeded; blocking...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> budget_id=agentgateway-system/budget-demo/demo-token-block
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> budget_action=&amp;#34;BLOCK&amp;#34; budget_unit=&amp;#34;TOKENS&amp;#34; budget_limit=2000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> budget_window=&amp;#34;DAILY&amp;#34; phase=&amp;#34;admission&amp;#34; outcome=&amp;#34;over_limit_block&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything you&amp;rsquo;d want in an alert: which budget, which action, which window,
and the outcome. An &lt;code>Audit&lt;/code> entry produces the same telemetry with a
non-blocking outcome — which is exactly how you should roll budgets out:
audit first, watch the logs for a week, then flip to &lt;code>Block&lt;/code>.&lt;/p>
&lt;h2 id="dimensions-scoping-budgets-to-teams-and-users">Dimensions: scoping budgets to teams and users&lt;/h2>
&lt;p>The demo entries above are unscoped — they apply to every request on the
enforced route. The &lt;code>subject&lt;/code> field is where multi-tenancy comes in. Take the
classic real-world split: engineering runs agents and evals all day and gets a
$500/month allocation; product does lighter prototyping on $200/month; and a
catch-all audit entry watches everything else:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budgets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">eng-monthly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">engineering &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># up to 16 key/value dimensions per entry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Month&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">product-monthly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">product&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Month&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">everyone-else&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># any request that carries a group dimension&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Month&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Engineering blowing through its allocation on a runaway eval loop gets 429s;
product keeps shipping, unaffected — same gateway, same route, different
wallets. A subject scopes the entry to requests whose resolved dimension values
match every key/value pair (a value of &lt;code>&amp;quot;*&amp;quot;&lt;/code> means &amp;ldquo;any non-missing value&amp;rdquo;). Combined
with the discovery modes on the enforcement policy (&lt;code>Same&lt;/code> / &lt;code>Selector&lt;/code> /
&lt;code>All&lt;/code>), the intended shape is clear: platform team owns the enforcement policy
on the gateway; each product team owns Budget resources in their own
namespace, scoped to their own dimensions. Org → team → user hierarchies with
different windows and actions per level.&lt;/p>
&lt;p>One honest caveat for day one: how dimension values are &lt;em>resolved&lt;/em> at request
time (trusted identity headers derived from JWT claims, per Solo&amp;rsquo;s cost-controls
reference architecture) isn&amp;rsquo;t fully documented yet outside the API reference.
For unscoped budgets like this demo, none of that matters — they just work.&lt;/p>
&lt;h2 id="gotchas-from-the-field">Gotchas from the field&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>The API group is &lt;code>enterpriseagentgateway.solo.io/v1alpha1&lt;/code>.&lt;/strong> The docs'
model-catalog example shows &lt;code>agentgateway.dev/v1alpha1&lt;/code> for the Parameters
kind; the CRDs actually installed by the v2026.6.3 charts use the
enterprise group. When in doubt, &lt;code>kubectl get crd | grep -i budget&lt;/code> and read
the schema — it also encodes useful validation (budget names must be
alphanumeric-with-hyphens; subject keys/values can&amp;rsquo;t contain &lt;code>|&lt;/code>, &lt;code>^&lt;/code>, or
backticks; token budgets cap at 2^32−1).&lt;/li>
&lt;li>&lt;strong>Attaching &lt;code>parametersRef&lt;/code> restarts the proxy.&lt;/strong> Plan for a rollout, not a
hot reload.&lt;/li>
&lt;li>&lt;strong>Budgets count input + output tokens.&lt;/strong> Size token budgets against
&lt;code>total_tokens&lt;/code>, not your prompt sizes.&lt;/li>
&lt;li>&lt;strong>Windows are rolling, not calendar-aligned.&lt;/strong> Budget enforcement rides the
gateway&amp;rsquo;s rate limiting infrastructure, so a tripped &lt;code>Day&lt;/code> budget stays
blocked until enough spend ages out of the trailing 24 hours — it does not
reset at midnight. Either way, never put a small blocking budget on a route
that shared infrastructure (agents, CI) depends on. Dedicated routes or
subject-scoped entries are your friends.&lt;/li>
&lt;li>&lt;strong>v2026.6.x is not a long-term-support train.&lt;/strong> AgentGateway moved to calver
with this cycle; quarterly releases (first expected: 2026.7.x) get 12 months
of patches. Fine for a lab, plan accordingly for production.&lt;/li>
&lt;/ul>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>Budget enforcement at the gateway is the missing primitive for AI platform
FinOps: the place that already sees every request, every token, and (with a
cost catalog) every dollar is now the place that can say &lt;em>no&lt;/em> — declaratively,
per team, per window, in the currency your CFO speaks. The rollout path is
gentle: load a catalog, watch &lt;code>agw.ai.usage.cost.total&lt;/code> show up in your logs,
add &lt;code>Audit&lt;/code> budgets, and only then arm &lt;code>Block&lt;/code> where runaway spend would
actually hurt.&lt;/p>
&lt;p>Total cost of this entire demo, per the gateway&amp;rsquo;s own accounting: about two
cents.&lt;/p></description><content:encoded>&lt;p>Enterprise AgentGateway v2026.6.3 shipped on July 1st with a changelog line that
FinOps-minded platform teams have been waiting for: &lt;strong>Enterprise Budgets and
Dimensions&lt;/strong>. Until now, &amp;ldquo;budgeting&amp;rdquo; LLM traffic through a gateway meant
approximating it with token-based rate limiting — workable, but request-window
based, tokens-only, and fiddly to reason about. The new release replaces that
with a first-class Kubernetes resource: &lt;code>EnterpriseAgentgatewayBudget&lt;/code>, a
declarative spending limit in &lt;strong>US dollars or tokens&lt;/strong>, accumulated over a
&lt;strong>rolling window&lt;/strong> (day, week, month, year), with a choice of &lt;strong>audit&lt;/strong> or
&lt;strong>block&lt;/strong> when the budget runs dry.&lt;/p>
&lt;p>This post is a day-one field report. The docs for the feature are still
catching up with the release (only the API reference covers it as I write
this), so everything below was derived from the live CRD schemas on my cluster
and verified with real traffic — including tripping a budget on purpose and
watching the gateway return &lt;code>429&lt;/code> with the window in the reset header.&lt;/p>
&lt;h2 id="why-budgets-not-rate-limits">Why budgets, not rate limits&lt;/h2>
&lt;p>Rate limits answer &amp;ldquo;how fast?&amp;rdquo; Budgets answer &amp;ldquo;how much, in dollars, this
month?&amp;rdquo; — which is the question your finance team actually asks. The
difference shows up in three places:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Unit.&lt;/strong> A budget is denominated in &lt;code>USD&lt;/code> or &lt;code>Tokens&lt;/code>. USD budgets use a
&lt;em>model cost catalog&lt;/em> (per-model input/output rates) so the gateway computes
the realized cost of every request as it happens.&lt;/li>
&lt;li>&lt;strong>Window.&lt;/strong> Budgets accumulate over &lt;strong>rolling windows&lt;/strong> — &lt;code>Day&lt;/code>, &lt;code>Week&lt;/code>,
&lt;code>Month&lt;/code>, &lt;code>Year&lt;/code>. Under the hood, enforcement rides the gateway&amp;rsquo;s rate
limiting infrastructure, so spend ages out continuously rather than
resetting at a calendar boundary: a &lt;code>Day&lt;/code> budget looks at the trailing 24
hours, not &amp;ldquo;since midnight.&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>Action.&lt;/strong> &lt;code>onBudgetExceeded: Audit&lt;/code> logs and lets traffic through (perfect
for rollout); &lt;code>Block&lt;/code> returns &lt;code>429&lt;/code> until enough spend rolls off the window
to bring you back under the limit. You can run both at once: a generous
audited dollar budget for visibility, plus a hard token stop as a circuit
breaker.&lt;/li>
&lt;/ul>
&lt;h2 id="the-moving-parts">The moving parts&lt;/h2>
&lt;p>Three resources cooperate, all in the &lt;code>enterpriseagentgateway.solo.io/v1alpha1&lt;/code>
API group:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>EnterpriseAgentgatewayBudget&lt;/code>&lt;/strong> — the budget definitions themselves. Each
entry has a &lt;code>name&lt;/code>, a &lt;code>limit&lt;/code> (&lt;code>unit&lt;/code> + &lt;code>amount&lt;/code>), a &lt;code>window&lt;/code>, an
&lt;code>onBudgetExceeded&lt;/code> action, and an optional &lt;code>subject&lt;/code> — up to &lt;strong>16 key/value
dimensions&lt;/strong> that scope the entry to matching requests (per user, team, API
key, model… &lt;code>&amp;quot;*&amp;quot;&lt;/code> matches any non-missing value; omit &lt;code>subject&lt;/code> and the
budget applies to every request on the enforced target).&lt;/li>
&lt;li>&lt;strong>&lt;code>EnterpriseAgentgatewayPolicy&lt;/code>&lt;/strong> with &lt;code>traffic.entBudgetEnforcement&lt;/code> —
switches enforcement on for the gateways or routes in &lt;code>targetRefs&lt;/code>, and
controls &lt;em>discovery&lt;/em>: which namespaces the controller scans for Budget
resources (&lt;code>Same&lt;/code>, &lt;code>Selector&lt;/code>, or &lt;code>All&lt;/code> — handy for letting each team keep
its own Budget objects in its own namespace).&lt;/li>
&lt;li>&lt;strong>&lt;code>EnterpriseAgentgatewayParameters&lt;/code>&lt;/strong> with &lt;code>modelCatalog&lt;/code> — loads per-model
pricing from a ConfigMap so USD budgets (and per-request cost telemetry)
have something to compute with.&lt;/li>
&lt;/ol>
&lt;h2 id="the-lab">The lab&lt;/h2>
&lt;p>My setup is a bare-metal Talos Kubernetes cluster running Solo Enterprise
AgentGateway v2026.6.3, fully GitOps-managed by ArgoCD — every manifest below
lives in a repo and lands via auto-sync. The main gateway (&lt;code>agentgateway-proxy&lt;/code>)
already fronts OpenAI at &lt;code>/openai&lt;/code>.&lt;/p>
&lt;p>One design note worth stealing: my &lt;code>/openai&lt;/code> route is also the default model
path for kagent agents in the same cluster. A blocking budget that trips would
cut those agents off for up to 24 hours. So the demo gets a &lt;strong>dedicated
route&lt;/strong> — same backend, different path — and the budget policy targets only
that route. Blast radius: zero.&lt;/p>
&lt;h2 id="step-1--the-model-cost-catalog">Step 1 — the model cost catalog&lt;/h2>
&lt;p>Per-model USD rates, per &lt;strong>1M tokens&lt;/strong>, as a ConfigMap:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">model-cost-catalog&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">catalog.json&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;providers&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;openai&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;models&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;gpt-4o&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;rates&amp;#34;: { &amp;#34;input&amp;#34;: &amp;#34;2.50&amp;#34;, &amp;#34;output&amp;#34;: &amp;#34;10.00&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;gpt-4o-mini&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> &amp;#34;rates&amp;#34;: { &amp;#34;input&amp;#34;: &amp;#34;0.15&amp;#34;, &amp;#34;output&amp;#34;: &amp;#34;0.60&amp;#34; }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> }&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Treat pricing as versioned configuration — when a provider changes rates, you
change a file in Git and your cost telemetry follows.&lt;/p>
&lt;h2 id="step-2--load-the-catalog-into-the-proxy">Step 2 — load the catalog into the proxy&lt;/h2>
&lt;p>The catalog attaches to a gateway through &lt;code>EnterpriseAgentgatewayParameters&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy-params&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">model-cost-catalog&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">catalog.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>…and the Gateway references it via &lt;code>infrastructure.parametersRef&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">infrastructure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parametersRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy-params&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Heads-up: attaching parameters rolls the proxy Deployment (a new pod came up in
seconds in my lab). On startup the proxy confirms the catalog load:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">info&lt;/span> &lt;span class="n">llm&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="n">cost&lt;/span> &lt;span class="n">watching&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">catalog&lt;/span> &lt;span class="n">files&lt;/span> &lt;span class="n">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">info&lt;/span> &lt;span class="n">llm&lt;/span>&lt;span class="p">::&lt;/span>&lt;span class="n">cost&lt;/span> &lt;span class="n">loaded&lt;/span> &lt;span class="n">model&lt;/span> &lt;span class="n">catalog&lt;/span> &lt;span class="n">providers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span> &lt;span class="n">models&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">80&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note &lt;code>providers=3 models=80&lt;/code> — your catalog merges with a built-in one, so you
only need to define models where you want to pin your own rates.&lt;/p>
&lt;h2 id="step-3--an-isolated-demo-route">Step 3 — an isolated demo route&lt;/h2>
&lt;p>Same OpenAI backend, dedicated path:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4--the-budgets-and-their-enforcement">Step 4 — the budgets and their enforcement&lt;/h2>
&lt;p>Two entries: a $5/day audited dollar budget (visibility) and a deliberately
tiny 2,000-token/day blocking budget (the circuit breaker we&amp;rsquo;re going to trip):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayBudget&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budgets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">demo-usd-audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">demo-token-block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Day&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo-enforcement&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">budget-demo&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traffic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entBudgetEnforcement&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">discovery&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Same&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Push, let ArgoCD sync, and check the policy attached:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ kubectl -n agentgateway-system get enterpriseagentgatewaypolicy budget-demo-enforcement
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">NAME ACCEPTED ATTACHED AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">budget-demo-enforcement True True 29s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="tripping-it">Tripping it&lt;/h2>
&lt;p>First request — normal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ curl -s http://&amp;lt;node-ip&amp;gt;:30160/budget-demo/v1/chat/completions \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -H &amp;#39;Content-Type: application/json&amp;#39; \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -d &amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;gpt-4o&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;In one sentence, what is a token budget?&amp;#34;}],&amp;#34;max_tokens&amp;#34;:60}&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">HTTP/1.1 200 OK
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;usage&amp;#34;:{&amp;#34;prompt_tokens&amp;#34;:17,&amp;#34;completion_tokens&amp;#34;:39,&amp;#34;total_tokens&amp;#34;:56}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then a few large completions (&lt;code>max_tokens: 800&lt;/code>, a 500-word essay each) to burn
the 2,000-token daily budget:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">request 1: HTTP/1.1 200 OK &amp;#34;total_tokens&amp;#34;:704
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">request 2: HTTP/1.1 200 OK &amp;#34;total_tokens&amp;#34;:711
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">request 3: HTTP/1.1 200 OK &amp;#34;total_tokens&amp;#34;:669
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">request 4: HTTP/1.1 429 Too Many Requests
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>56 + 704 + 711 + 669 = 2,140 tokens — over budget. Request 4 never reaches
OpenAI. The &lt;code>429&lt;/code> response is admission-level and self-describing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">HTTP/1.1 429 Too Many Requests
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">x-ratelimit-limit: 2000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">x-ratelimit-remaining: 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">x-ratelimit-reset: 86400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rate limit exceeded
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>x-ratelimit-reset: 86400&lt;/code> — the 24-hour rolling window. Clients get exactly
what they need to back off intelligently.&lt;/p>
&lt;p>Meanwhile the production &lt;code>/openai&lt;/code> route (which kagent&amp;rsquo;s agents use) kept
returning &lt;code>200&lt;/code> throughout — the budget is scoped to the demo route and
nothing else.&lt;/p>
&lt;h2 id="what-you-get-in-the-logs">What you get in the logs&lt;/h2>
&lt;p>This is my favorite part. With the catalog loaded, &lt;strong>every LLM request logs
its realized dollar cost&lt;/strong> alongside the usual token telemetry:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">info request route=agentgateway-system/budget-demo ... http.status=200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gen_ai.request.model=gpt-4o gen_ai.response.model=gpt-4o-2024-08-06
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gen_ai.usage.input_tokens=21 gen_ai.usage.output_tokens=683
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agw.ai.usage.cost.total=0.0068825
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s $0.0069 for a 704-token gpt-4o call, computed from the catalog at
request time — the same number your USD budgets accrue against, and it flows
into traces and metrics for your observability stack.&lt;/p>
&lt;p>And when the blocking budget trips, the gateway says so in plain terms:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">warn budget budget exceeded; blocking...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> budget_id=agentgateway-system/budget-demo/demo-token-block
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> budget_action=&amp;#34;BLOCK&amp;#34; budget_unit=&amp;#34;TOKENS&amp;#34; budget_limit=2000
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> budget_window=&amp;#34;DAILY&amp;#34; phase=&amp;#34;admission&amp;#34; outcome=&amp;#34;over_limit_block&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything you&amp;rsquo;d want in an alert: which budget, which action, which window,
and the outcome. An &lt;code>Audit&lt;/code> entry produces the same telemetry with a
non-blocking outcome — which is exactly how you should roll budgets out:
audit first, watch the logs for a week, then flip to &lt;code>Block&lt;/code>.&lt;/p>
&lt;h2 id="dimensions-scoping-budgets-to-teams-and-users">Dimensions: scoping budgets to teams and users&lt;/h2>
&lt;p>The demo entries above are unscoped — they apply to every request on the
enforced route. The &lt;code>subject&lt;/code> field is where multi-tenancy comes in. Take the
classic real-world split: engineering runs agents and evals all day and gets a
$500/month allocation; product does lighter prototyping on $200/month; and a
catch-all audit entry watches everything else:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">budgets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">eng-monthly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">engineering &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># up to 16 key/value dimensions per entry&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Month&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">product-monthly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">product&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Month&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Block&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">everyone-else&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">subject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># any request that carries a group dimension&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">USD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">amount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">window&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Month&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onBudgetExceeded&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Audit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Engineering blowing through its allocation on a runaway eval loop gets 429s;
product keeps shipping, unaffected — same gateway, same route, different
wallets. A subject scopes the entry to requests whose resolved dimension values
match every key/value pair (a value of &lt;code>&amp;quot;*&amp;quot;&lt;/code> means &amp;ldquo;any non-missing value&amp;rdquo;). Combined
with the discovery modes on the enforcement policy (&lt;code>Same&lt;/code> / &lt;code>Selector&lt;/code> /
&lt;code>All&lt;/code>), the intended shape is clear: platform team owns the enforcement policy
on the gateway; each product team owns Budget resources in their own
namespace, scoped to their own dimensions. Org → team → user hierarchies with
different windows and actions per level.&lt;/p>
&lt;p>One honest caveat for day one: how dimension values are &lt;em>resolved&lt;/em> at request
time (trusted identity headers derived from JWT claims, per Solo&amp;rsquo;s cost-controls
reference architecture) isn&amp;rsquo;t fully documented yet outside the API reference.
For unscoped budgets like this demo, none of that matters — they just work.&lt;/p>
&lt;h2 id="gotchas-from-the-field">Gotchas from the field&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>The API group is &lt;code>enterpriseagentgateway.solo.io/v1alpha1&lt;/code>.&lt;/strong> The docs'
model-catalog example shows &lt;code>agentgateway.dev/v1alpha1&lt;/code> for the Parameters
kind; the CRDs actually installed by the v2026.6.3 charts use the
enterprise group. When in doubt, &lt;code>kubectl get crd | grep -i budget&lt;/code> and read
the schema — it also encodes useful validation (budget names must be
alphanumeric-with-hyphens; subject keys/values can&amp;rsquo;t contain &lt;code>|&lt;/code>, &lt;code>^&lt;/code>, or
backticks; token budgets cap at 2^32−1).&lt;/li>
&lt;li>&lt;strong>Attaching &lt;code>parametersRef&lt;/code> restarts the proxy.&lt;/strong> Plan for a rollout, not a
hot reload.&lt;/li>
&lt;li>&lt;strong>Budgets count input + output tokens.&lt;/strong> Size token budgets against
&lt;code>total_tokens&lt;/code>, not your prompt sizes.&lt;/li>
&lt;li>&lt;strong>Windows are rolling, not calendar-aligned.&lt;/strong> Budget enforcement rides the
gateway&amp;rsquo;s rate limiting infrastructure, so a tripped &lt;code>Day&lt;/code> budget stays
blocked until enough spend ages out of the trailing 24 hours — it does not
reset at midnight. Either way, never put a small blocking budget on a route
that shared infrastructure (agents, CI) depends on. Dedicated routes or
subject-scoped entries are your friends.&lt;/li>
&lt;li>&lt;strong>v2026.6.x is not a long-term-support train.&lt;/strong> AgentGateway moved to calver
with this cycle; quarterly releases (first expected: 2026.7.x) get 12 months
of patches. Fine for a lab, plan accordingly for production.&lt;/li>
&lt;/ul>
&lt;h2 id="wrapping-up">Wrapping up&lt;/h2>
&lt;p>Budget enforcement at the gateway is the missing primitive for AI platform
FinOps: the place that already sees every request, every token, and (with a
cost catalog) every dollar is now the place that can say &lt;em>no&lt;/em> — declaratively,
per team, per window, in the currency your CFO speaks. The rollout path is
gentle: load a catalog, watch &lt;code>agw.ai.usage.cost.total&lt;/code> show up in your logs,
add &lt;code>Audit&lt;/code> budgets, and only then arm &lt;code>Block&lt;/code> where runaway spend would
actually hurt.&lt;/p>
&lt;p>Total cost of this entire demo, per the gateway&amp;rsquo;s own accounting: about two
cents.&lt;/p></content:encoded></item><item><title>Three Ways to Combine agentgateway with F5 AI Guardrails (and Where F5 Distributed Cloud Fits)</title><link>https://maniak.io/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/</guid><description>&lt;p>F5&amp;rsquo;s acquisition of CalypsoAI gave enterprises something a lot of security
teams have been asking for: a dedicated AI runtime security layer — &lt;strong>F5 AI
Guardrails&lt;/strong> — that scans prompts and responses against policy (prompt
injection, jailbreaks, PII, data exfiltration) independently of whichever
model or app framework is behind it. Meanwhile, platform teams are
standardizing on &lt;strong>agentgateway&lt;/strong> as the data plane for LLM, MCP, and A2A
traffic: one OpenAI-compatible front door with routing, failover, auth, spend
limits, and OpenTelemetry.&lt;/p>
&lt;p>So the obvious question came up — in my case, from an enterprise that already
runs its web estate behind F5 Distributed Cloud (XC): &lt;strong>can these two coexist,
and who sits in front of whom?&lt;/strong>&lt;/p>
&lt;p>Short answer: yes, and there are three workable architectures. I verified the
key API surfaces against the live F5 docs (&lt;a href="https://docs.aisecurity.f5.com/">docs.aisecurity.f5.com&lt;/a>
— note that &lt;code>docs.calypsoai.com&lt;/code> now redirects there) rather than assuming,
because the whole design hinges on two questions:&lt;/p>
&lt;ol>
&lt;li>Does Guardrails expose an &lt;strong>OpenAI-compatible inline endpoint&lt;/strong>? &lt;em>(Yes:
&lt;code>POST /openai/{provider}/chat/completions&lt;/code>.)&lt;/em>&lt;/li>
&lt;li>Can you &lt;strong>override the backend URL&lt;/strong> Guardrails forwards to? &lt;em>(Yes:
providers are created with &lt;code>POST /backend/v1/providers&lt;/code> and carry a
&lt;code>template.url&lt;/code> — an arbitrary endpoint.)&lt;/em>&lt;/li>
&lt;/ol>
&lt;p>That second one matters because it decides whether &amp;ldquo;agentgateway behind F5&amp;rdquo;
is even possible. It is. Let&amp;rsquo;s walk through all three options.&lt;/p>
&lt;h2 id="first-get-the-product-boundaries-right">First, get the product boundaries right&lt;/h2>
&lt;p>One thing that trips people up: &lt;strong>F5 AI Guardrails is not an F5 XC feature.&lt;/strong>
It&amp;rsquo;s the CalypsoAI platform under F5 branding — its own console, its own
projects and API tokens, delivered as SaaS (&lt;code>us1.calypsoai.app&lt;/code> /
&lt;code>eu1.calypsoai.app&lt;/code>) or self-hosted on Kubernetes/OpenShift. Your XC tenant
gives you the WAAP edge (TLS, WAF, bot defense, DDoS, API protection); the
Guardrails license is separate.&lt;/p>
&lt;p>The three products in play:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Product&lt;/th>
&lt;th>Job&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Edge&lt;/td>
&lt;td>F5 Distributed Cloud HTTP LB&lt;/td>
&lt;td>Public entry: TLS, WAF, bot defense, DDoS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI data plane&lt;/td>
&lt;td>agentgateway&lt;/td>
&lt;td>OpenAI-compatible routing, failover, authn, budgets, MCP/A2A, OTel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI security&lt;/td>
&lt;td>F5 AI Guardrails (ex-CalypsoAI)&lt;/td>
&lt;td>Prompt/response scanning: inject, jailbreak, PII, custom scanners&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Guardrails itself supports two consumption modes, and every architecture below
is a composition of them:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Inline&lt;/strong> — Guardrails terminates the request, scans, and forwards to a
configured provider. Speaks its native PromptAPI (&lt;code>POST /backend/v1/prompts&lt;/code>)
&lt;em>or&lt;/em> plain OpenAI chat completions (&lt;code>POST /openai/{provider}/chat/completions&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Out-of-band&lt;/strong> — a pure verdict API. &lt;code>POST /backend/v1/scans&lt;/code> takes
&lt;code>input&lt;/code>, &lt;code>project&lt;/code>, and &lt;code>scanDirection&lt;/code> (&lt;code>request&lt;/code> or &lt;code>response&lt;/code>) and returns
an outcome plus &lt;code>redactedInput&lt;/code>. Nothing gets forwarded; &lt;em>you&lt;/em> stay in charge
of calling the model.&lt;/li>
&lt;/ul>
&lt;h2 id="why-the-gateway-layer-is-non-negotiable">Why the gateway layer is non-negotiable&lt;/h2>
&lt;p>Before the architectures, it&amp;rsquo;s worth being blunt about why agentgateway is in
every one of these diagrams — because &amp;ldquo;can&amp;rsquo;t the apps just call Guardrails
directly?&amp;rdquo; is the first question you&amp;rsquo;ll get in an architecture review. They
can (that&amp;rsquo;s Lab 3 of the F5 workshop), and it doesn&amp;rsquo;t scale. Every other
layer in the stack assumes someone else is governing AI traffic: the XC edge
governs HTTP and doesn&amp;rsquo;t know what a token, a model, or a tool call is;
Guardrails judges &lt;em>content&lt;/em> and doesn&amp;rsquo;t route, failover, meter, or
authenticate your apps. Without a gateway, your &amp;ldquo;AI platform&amp;rdquo; is a pile of
SDK calls nobody owns.&lt;/p>
&lt;p>What the gateway layer uniquely provides:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>One front door instead of N×M integrations.&lt;/strong> Ten apps on three
providers is thirty credential sets and thirty places to touch every time
a model deprecates. Behind one OpenAI-compatible endpoint, providers and
models change without a single app redeploy.&lt;/li>
&lt;li>&lt;strong>Provider keys leave the apps.&lt;/strong> Apps authenticate to &lt;em>you&lt;/em> (JWT, API
key, OAuth); OpenAI/Anthropic credentials exist in exactly one place.
Revoking a team&amp;rsquo;s access takes seconds and doesn&amp;rsquo;t rotate a provider key
across the fleet.&lt;/li>
&lt;li>&lt;strong>Spend control that actually blocks.&lt;/strong> Provider dashboards report last
month; gateway budgets are enforced live — dollar- or token-denominated,
per team or app, returning &lt;code>429&lt;/code> when the money runs out. The edge can
rate-limit requests, but a request can cost $0.001 or $5; only the layer
that parses usage can meter dollars.&lt;/li>
&lt;li>&lt;strong>Failover and model agility.&lt;/strong> Provider outages and deprecations are
&lt;em>when&lt;/em>, not &lt;em>if&lt;/em>. Automatic cross-provider failover and model aliasing
turn every provider incident from an all-hands app change into a config
edit.&lt;/li>
&lt;li>&lt;strong>MCP and A2A governance.&lt;/strong> Agents call tools, and ungoverned MCP servers
are the new shadow IT — every tool an agent can reach is an exfiltration
path. agentgateway federates MCP servers behind auth and RBAC; neither
the edge nor Guardrails addresses this at all.&lt;/li>
&lt;li>&lt;strong>Observability where the semantics live.&lt;/strong> OTel traces with tokens,
cost, model, and latency per request. The edge sees bytes; the guardrail
sees verdicts; only the gateway sees the whole conversation.&lt;/li>
&lt;li>&lt;strong>It&amp;rsquo;s the socket the guardrail plugs into.&lt;/strong> Without a gateway, adopting
Guardrails means every app team writes scan-then-forward code. With one,
guardrails become a &lt;em>policy&lt;/em> — one webhook config, zero app changes
(that&amp;rsquo;s Option C below).&lt;/li>
&lt;/ol>
&lt;p>And the honest trade-offs: it&amp;rsquo;s another hop (single-digit milliseconds —
noise next to seconds of LLM inference) and another component to operate; if
it&amp;rsquo;s down, AI traffic is down (it&amp;rsquo;s a stateless Rust proxy built to run as
HA replicas); the budget/webhook/UI features are in the enterprise tier. The
counterargument to all three is the same: without the gateway you end up
building ad-hoc versions of half these features anyway — badly, in every
app, with no one accountable.&lt;/p>
&lt;p>You wouldn&amp;rsquo;t run web apps without a load balancer or APIs without an API
gateway. Running LLMs and agents without an AI gateway is the same mistake,
except the blast radius is your provider bill, your credentials, and every
tool your agents can touch. The edge protects you from the internet;
Guardrails protects you from the content; &lt;strong>the gateway is what makes AI
traffic governable at all.&lt;/strong>&lt;/p>
&lt;h2 id="option-a--agentgateway-in-front-of-guardrails">Option A — agentgateway in front of Guardrails&lt;/h2>
&lt;p>The zero-code option. Guardrails&amp;rsquo; inline endpoint is OpenAI-compatible, so
agentgateway just treats it as one more LLM backend. F5 makes the final hop to
the provider.&lt;/p>
&lt;div class="mermaid">flowchart LR
 C[&amp;#34;AI apps / agents&amp;lt;br/&amp;gt;(OpenAI SDK)&amp;#34;] --&amp;gt; AGW[&amp;#34;agentgateway&amp;lt;br/&amp;gt;authn · routing · budgets · OTel&amp;#34;]
 AGW --&amp;gt; GR[&amp;#34;F5 AI Guardrails (inline)&amp;lt;br/&amp;gt;/openai/openai-prod/chat/completions&amp;lt;br/&amp;gt;scans prompt + response&amp;#34;]
 GR --&amp;gt; LLM[&amp;#34;LLM provider&amp;lt;br/&amp;gt;(configured in Guardrails)&amp;#34;]
&lt;/div>
&lt;p>The flow, end to end:&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant App as AI app
 participant AGW as agentgateway
 participant GR as F5 AI Guardrails
 participant LLM as LLM provider

 App-&amp;gt;&amp;gt;AGW: POST /v1/chat/completions
 AGW-&amp;gt;&amp;gt;AGW: authn, rate limit, model alias, budget
 AGW-&amp;gt;&amp;gt;GR: POST /openai/{provider}/chat/completions
 GR-&amp;gt;&amp;gt;GR: scan prompt (inject, jailbreak, PII...)
 alt prompt blocked
 GR--&amp;gt;&amp;gt;AGW: policy violation
 AGW--&amp;gt;&amp;gt;App: error (logged + traced)
 else prompt clear
 GR-&amp;gt;&amp;gt;LLM: forward (transformed to provider spec)
 LLM--&amp;gt;&amp;gt;GR: completion
 GR-&amp;gt;&amp;gt;GR: scan response
 GR--&amp;gt;&amp;gt;AGW: sanitized completion
 AGW--&amp;gt;&amp;gt;App: completion (+ usage, cost, trace)
 end
&lt;/div>
&lt;p>Configuration is just a custom OpenAI-compatible backend in agentgateway
pointing at the Guardrails host, with the CalypsoAI token as backend auth. The
same mechanics F5 documents for pointing the raw OpenAI SDK at Guardrails
(&lt;code>base_url = &amp;quot;{BASE_URL}/openai/{CONNECTION_NAME}&amp;quot;&lt;/code>) apply — agentgateway is
simply the client.&lt;/p>
&lt;p>&lt;strong>Choose this when&lt;/strong> the security team owns model access end-to-end and you
want the fastest possible integration. &lt;strong>Trade-off:&lt;/strong> the &lt;em>final&lt;/em> provider
choice lives in Guardrails&amp;rsquo; provider config, so agentgateway&amp;rsquo;s multi-provider
failover happens between Guardrails connections rather than directly against
the LLMs.&lt;/p>
&lt;h2 id="option-b--agentgateway-behind-guardrails">Option B — agentgateway behind Guardrails&lt;/h2>
&lt;p>The reverse: Guardrails is the client-facing boundary and delegates the actual
model plumbing to agentgateway. This only works if you can override the
endpoint Guardrails forwards to — and you can. A Guardrails &lt;strong>provider&lt;/strong> is
defined by a request template with a &lt;code>url&lt;/code>, &lt;code>method&lt;/code>, &lt;code>headers&lt;/code>, and body
mapping, so you point that template at agentgateway&amp;rsquo;s OpenAI-compatible
listener:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">POST&lt;/span> &lt;span class="err">/backend/v&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="err">/providers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;template&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;request&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://agw.internal.example.com/v1/chat/completions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;headers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;Authorization&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Bearer {{agw_token}}&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;secrets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;agw_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;projectId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;guardrails-project&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="mermaid">flowchart LR
 C[&amp;#34;AI apps / agents&amp;#34;] --&amp;gt; GR[&amp;#34;F5 AI Guardrails (inline)&amp;lt;br/&amp;gt;terminates + scans&amp;#34;]
 GR --&amp;gt;|&amp;#34;provider template.url →&amp;#34;| AGW[&amp;#34;agentgateway&amp;#34;]
 AGW --&amp;gt; L1[&amp;#34;OpenAI&amp;#34;]
 AGW --&amp;gt; L2[&amp;#34;Anthropic&amp;#34;]
 AGW --&amp;gt; L3[&amp;#34;vLLM (self-hosted)&amp;#34;]
 AGW --&amp;gt; MCP[&amp;#34;MCP tool servers&amp;#34;]
&lt;/div>
&lt;div class="mermaid">sequenceDiagram
 participant App as AI app
 participant GR as F5 AI Guardrails
 participant AGW as agentgateway
 participant LLM as best provider

 App-&amp;gt;&amp;gt;GR: POST /openai/agentgateway/chat/completions
 GR-&amp;gt;&amp;gt;GR: scan prompt
 GR-&amp;gt;&amp;gt;AGW: forward via provider template.url
 AGW-&amp;gt;&amp;gt;AGW: route, failover, budget, trace
 AGW-&amp;gt;&amp;gt;LLM: provider-native request
 LLM--&amp;gt;&amp;gt;AGW: completion
 AGW--&amp;gt;&amp;gt;GR: OpenAI-format response
 GR-&amp;gt;&amp;gt;GR: scan response
 GR--&amp;gt;&amp;gt;App: sanitized completion
&lt;/div>
&lt;p>&lt;strong>Choose this when&lt;/strong> the security team insists on owning the client-facing
endpoint, but you still want gateway-grade routing/failover/spend control
underneath the guardrail check. &lt;strong>Trade-off:&lt;/strong> the provider-template mechanism
is documented, but chaining it into another gateway isn&amp;rsquo;t an F5-published
pattern — validate streaming pass-through and tool/function-call payloads in a
spike before committing.&lt;/p>
&lt;h2 id="option-c--out-of-band-agentgateway-calls-guardrails-as-a-scanner">Option C — out-of-band: agentgateway calls Guardrails as a scanner&lt;/h2>
&lt;p>My favorite long-term shape. agentgateway stays the single inference path and
invokes Guardrails&amp;rsquo; ScanAPI as a side-scan in both directions, using
agentgateway&amp;rsquo;s &lt;strong>webhook prompt guards&lt;/strong>. Guardrails never proxies anything;
it just renders verdicts. The only code you write is a thin adapter
(~100 lines) that translates agentgateway&amp;rsquo;s webhook contract to
&lt;code>POST /backend/v1/scans&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Guardrails outcome = flagged → webhook &lt;strong>rejects&lt;/strong> (client gets a 4xx)&lt;/li>
&lt;li>Guardrails returns &lt;code>redactedInput&lt;/code> → webhook &lt;strong>modifies&lt;/strong> (masked content
goes forward)&lt;/li>
&lt;li>otherwise → &lt;strong>allow&lt;/strong>&lt;/li>
&lt;/ul>
&lt;div class="mermaid">flowchart LR
 C[&amp;#34;AI apps / agents&amp;#34;] --&amp;gt; AGW[&amp;#34;agentgateway&amp;#34;]
 AGW -. &amp;#34;request webhook&amp;#34; .-&amp;gt; AD[&amp;#34;guardrails adapter&amp;lt;br/&amp;gt;(~100 lines)&amp;#34;]
 AD -. &amp;#34;POST /backend/v1/scans&amp;lt;br/&amp;gt;scanDirection=request&amp;#34; .-&amp;gt; GR[&amp;#34;F5 AI Guardrails&amp;lt;br/&amp;gt;projects · scanners · audit&amp;#34;]
 AGW --&amp;gt; LLM[&amp;#34;LLM providers&amp;#34;]
 AGW -. &amp;#34;response webhook&amp;#34; .-&amp;gt; AD
&lt;/div>
&lt;div class="mermaid">sequenceDiagram
 participant App as AI app
 participant AGW as agentgateway
 participant AD as adapter
 participant GR as F5 Guardrails
 participant LLM as LLM provider

 App-&amp;gt;&amp;gt;AGW: POST /v1/chat/completions
 AGW-&amp;gt;&amp;gt;AD: promptGuard request webhook
 AD-&amp;gt;&amp;gt;GR: POST /backend/v1/scans (scanDirection=request)
 GR--&amp;gt;&amp;gt;AD: outcome + redactedInput
 alt flagged
 AD--&amp;gt;&amp;gt;AGW: reject
 AGW--&amp;gt;&amp;gt;App: 403 policy violation
 else cleared / redacted
 AD--&amp;gt;&amp;gt;AGW: allow or modify
 AGW-&amp;gt;&amp;gt;LLM: forward
 LLM--&amp;gt;&amp;gt;AGW: completion
 AGW-&amp;gt;&amp;gt;AD: promptGuard response webhook
 AD-&amp;gt;&amp;gt;GR: POST /backend/v1/scans (scanDirection=response)
 GR--&amp;gt;&amp;gt;AD: outcome
 AD--&amp;gt;&amp;gt;AGW: allow / modify / reject
 AGW--&amp;gt;&amp;gt;App: final response
 end
&lt;/div>
&lt;p>Wiring it up on the agentgateway side is a policy targeting your LLM route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">kind: Service, name: f5-guardrails-adapter, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">kind: Service, name: f5-guardrails-adapter, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Choose this when&lt;/strong> you want agentgateway to keep full provider control
(failover, streaming straight from the provider, budgets) while the security
team keeps full policy control in the Guardrails console — clean separation of
planes, one hop fewer on the token stream. &lt;strong>Trade-off:&lt;/strong> you own the adapter,
and scanning &lt;em>streamed&lt;/em> responses forces a buffering decision
(scan-on-complete vs. chunked) that you should prototype early. If this
pattern spreads, the natural product evolution is native F5 ScanAPI support in
agentgateway&amp;rsquo;s guardrail family, right next to Bedrock Guardrails and Model
Armor.&lt;/p>
&lt;h2 id="the-full-picture-f5-xc-on-the-edge">The full picture: F5 XC on the edge&lt;/h2>
&lt;p>If you already run F5 Distributed Cloud, the AI stack slots in behind it the
same way your web properties do. XC doesn&amp;rsquo;t host Guardrails — it contributes
the hardened public edge, and an origin pool pointing at agentgateway:&lt;/p>
&lt;div class="mermaid">flowchart TB
 U[&amp;#34;Internet clients / AI apps&amp;#34;] --&amp;gt; XC[&amp;#34;F5 XC HTTP Load Balancer&amp;lt;br/&amp;gt;TLS · WAF · bot defense · DDoS · API protection&amp;#34;]
 XC --&amp;gt;|origin pool| AGW[&amp;#34;agentgateway (K8s or XC CE site)&amp;lt;br/&amp;gt;LLM + MCP + A2A gateway&amp;#34;]
 AGW -. &amp;#34;Option C webhook&amp;#34; .-&amp;gt; AD[&amp;#34;guardrails adapter&amp;#34;]
 AD -.-&amp;gt; GR[&amp;#34;F5 AI Guardrails&amp;lt;br/&amp;gt;SaaS us1/eu1.calypsoai.app or self-hosted&amp;#34;]
 AGW --&amp;gt; P1[&amp;#34;OpenAI&amp;#34;]
 AGW --&amp;gt; P2[&amp;#34;Anthropic&amp;#34;]
 AGW --&amp;gt; P3[&amp;#34;vLLM / self-hosted&amp;#34;]
 AGW --&amp;gt; MCP[&amp;#34;MCP tool servers&amp;#34;]
&lt;/div>
&lt;p>Every layer does the one thing it&amp;rsquo;s best at: XC absorbs the internet,
agentgateway governs AI traffic, Guardrails judges content. And each layer is
independently swappable — which is exactly what you want when this space is
moving as fast as it is.&lt;/p>
&lt;h2 id="choosing-between-them">Choosing between them&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>A: agw → F5&lt;/th>
&lt;th>B: F5 → agw&lt;/th>
&lt;th>C: out-of-band&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Code required&lt;/td>
&lt;td>none&lt;/td>
&lt;td>none&lt;/td>
&lt;td>small adapter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client-facing endpoint&lt;/td>
&lt;td>agentgateway&lt;/td>
&lt;td>Guardrails&lt;/td>
&lt;td>agentgateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Provider routing/failover&lt;/td>
&lt;td>in Guardrails&lt;/td>
&lt;td>in agentgateway&lt;/td>
&lt;td>in agentgateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Extra proxy hop on token stream&lt;/td>
&lt;td>yes&lt;/td>
&lt;td>yes&lt;/td>
&lt;td>no (verdicts only)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Streaming risk&lt;/td>
&lt;td>F5-documented&lt;/td>
&lt;td>needs spike&lt;/td>
&lt;td>buffering choice on response scan&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Best for&lt;/td>
&lt;td>fastest PoC&lt;/td>
&lt;td>security-team-owned front door&lt;/td>
&lt;td>production, clean separation of planes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>My recommendation: &lt;strong>prove Option A in an afternoon&lt;/strong> (it&amp;rsquo;s configuration
only), then &lt;strong>build Option C for production&lt;/strong>. Option B is the niche play for
orgs whose security team must terminate the client connection.&lt;/p>
&lt;h2 id="runnable-lab-and-test-results">Runnable lab and test results&lt;/h2>
&lt;p>I put the working kind lab here:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai">github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai&lt;/a>&lt;/strong>&lt;/p>
&lt;p>The lab deploys both recommended paths:&lt;/p>
&lt;ul>
&lt;li>&lt;code>/option-a&lt;/code> routes from agentgateway to the F5 AI Guardrails
OpenAI-compatible inline endpoint.&lt;/li>
&lt;li>&lt;code>/option-c&lt;/code> routes from agentgateway directly to OpenAI, with F5 ScanAPI
called out-of-band from request and response &lt;code>promptGuard&lt;/code> webhooks.&lt;/li>
&lt;/ul>
&lt;p>The setup script creates two intentionally simple custom scanners in F5 AI
Guardrails:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scanner&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Match&lt;/th>
&lt;th>Direction&lt;/th>
&lt;th>Mode&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-codename&lt;/code>&lt;/td>
&lt;td>Keyword&lt;/td>
&lt;td>&lt;code>project-titan&lt;/code>&lt;/td>
&lt;td>Prompts and responses&lt;/td>
&lt;td>Block&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-ssn&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>&lt;code>\d{3}-\d{2}-\d{4}&lt;/code>&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;td>Redact&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That gives the test suite something concrete to prove. The smoke test lives in
&lt;code>test.sh&lt;/code> and sends six OpenAI Chat Completions requests through the gateway:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Test&lt;/th>
&lt;th>Route&lt;/th>
&lt;th>Expected result&lt;/th>
&lt;th>What it proves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Option A benign&lt;/td>
&lt;td>&lt;code>/option-a&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>&lt;/td>
&lt;td>agentgateway can reach the F5 inline OpenAI-compatible endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C benign&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>&lt;/td>
&lt;td>agentgateway can reach OpenAI directly while the webhook adapter passes clear prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option A blocked codename&lt;/td>
&lt;td>&lt;code>/option-a&lt;/code>&lt;/td>
&lt;td>&lt;code>400&lt;/code> or &lt;code>403&lt;/code>&lt;/td>
&lt;td>F5 inline scanning blocks the custom &lt;code>project-titan&lt;/code> keyword&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C blocked codename&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>403&lt;/code>&lt;/td>
&lt;td>the request webhook calls ScanAPI and rejects a blocked prompt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C SSN redaction&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>, no raw SSN&lt;/td>
&lt;td>ScanAPI returns &lt;code>redactedInput&lt;/code>, and the adapter replaces the user message before forwarding&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C response-phase mask&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>, no blocked keyword&lt;/td>
&lt;td>the response webhook scans the assistant output and masks blocked content&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The important design choice is that Option C tests both directions. Request
scanning catches unsafe user input before the model sees it; response scanning
catches unsafe model output before the client sees it.&lt;/p>
&lt;p>This is the traffic flow the tests exercise:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/agw-f5-traffic-flow.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/agw-f5-traffic-flow.gif" alt="Animated traffic flow for test.sh showing Option A inline F5 Guardrails traffic and Option C out-of-band ScanAPI traffic through agentgateway webhooks" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Run it after deployment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/202-agw-f5-ai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup-guardrails.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The passing output from my run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">PASS Option A benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option A blocked codename: HTTP 400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C blocked codename: HTTP 403
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C SSN redaction request completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C redaction did not leak raw SSN
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scan completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scanner masked blocked output
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The same run shows up in F5 AI Guardrails under &lt;strong>Logs → Prompt history&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/f5-logs-test-output.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/f5-logs-test-output.png" alt="F5 AI Guardrails prompt history showing blocked project-titan scans and redacted SSN scans from the agentgateway test run" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Those rows are the audit trail that matters in a real rollout:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Blocked / Scan / &lt;code>project-titan&lt;/code>&lt;/strong> — Option C&amp;rsquo;s request webhook called
ScanAPI before the model request and F5 returned a blocking verdict.&lt;/li>
&lt;li>&lt;strong>Redacted / Scan / &lt;code>Repeat back exactly: my ssn is ***********&lt;/code>&lt;/strong> — the
regex scanner caught the SSN pattern and returned redacted content instead of
the raw value.&lt;/li>
&lt;li>&lt;strong>Blocked / Prompt / Genai Azure Openai&lt;/strong> — Option A hit the F5 inline
OpenAI-compatible provider, so F5 logged the proxied prompt path as well as
the scan decision.&lt;/li>
&lt;/ul>
&lt;p>This is the operational difference between &amp;ldquo;the gateway returned the right
status code&amp;rdquo; and &amp;ldquo;security has evidence.&amp;rdquo; The terminal output proves
agentgateway enforced the policy; the Guardrails logs prove the scanner
decision, user, provider path, interaction text, and timestamp were recorded in
the F5 audit plane.&lt;/p>
&lt;p>There is also a fuller harness in &lt;code>run_harness.sh&lt;/code> that reads
&lt;code>harness/cases.yaml&lt;/code>, records latency/status/usage metadata, and writes
&lt;code>harness/results.jsonl&lt;/code>. Use &lt;code>test.sh&lt;/code> when you want a quick operational
answer; use the harness when you want repeatable evidence for a write-up,
demo, or CI job.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.aisecurity.f5.com/">F5 AI Guardrails API docs&lt;/a> — &lt;a href="https://docs.aisecurity.f5.com/api-docs/getting-started-defend.html">getting started with Defend&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/operations/post_scans.html">ScanAPI&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/operations/post_providers.html">providers&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/operations/post_openai_provider_chat_completions.html">OpenAI-compatible endpoint&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/integrations/proxy.openai-sdk.html">OpenAI SDK proxy integration&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai">Runnable Options A and C lab&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/f5devcentral/f5-ai-security-api-integration-examples">F5 AI security API integration examples (GitHub)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/latest/">agentgateway docs&lt;/a> — &lt;a href="https://docs.solo.io/agentgateway/latest/llm/providers/custom/">custom providers&lt;/a>, &lt;a href="https://docs.solo.io/agentgateway/latest/llm/guardrails/webhook/">webhook guardrails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://clouddocs.f5.com/training/community/f5xc-emea-workshop/html/class6/module1/module1.html">F5 XC EMEA workshop, Class 6 Module 1&lt;/a> — Lab 2 (inline), Lab 3 (out-of-band)&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>F5&amp;rsquo;s acquisition of CalypsoAI gave enterprises something a lot of security
teams have been asking for: a dedicated AI runtime security layer — &lt;strong>F5 AI
Guardrails&lt;/strong> — that scans prompts and responses against policy (prompt
injection, jailbreaks, PII, data exfiltration) independently of whichever
model or app framework is behind it. Meanwhile, platform teams are
standardizing on &lt;strong>agentgateway&lt;/strong> as the data plane for LLM, MCP, and A2A
traffic: one OpenAI-compatible front door with routing, failover, auth, spend
limits, and OpenTelemetry.&lt;/p>
&lt;p>So the obvious question came up — in my case, from an enterprise that already
runs its web estate behind F5 Distributed Cloud (XC): &lt;strong>can these two coexist,
and who sits in front of whom?&lt;/strong>&lt;/p>
&lt;p>Short answer: yes, and there are three workable architectures. I verified the
key API surfaces against the live F5 docs (&lt;a href="https://docs.aisecurity.f5.com/">docs.aisecurity.f5.com&lt;/a>
— note that &lt;code>docs.calypsoai.com&lt;/code> now redirects there) rather than assuming,
because the whole design hinges on two questions:&lt;/p>
&lt;ol>
&lt;li>Does Guardrails expose an &lt;strong>OpenAI-compatible inline endpoint&lt;/strong>? &lt;em>(Yes:
&lt;code>POST /openai/{provider}/chat/completions&lt;/code>.)&lt;/em>&lt;/li>
&lt;li>Can you &lt;strong>override the backend URL&lt;/strong> Guardrails forwards to? &lt;em>(Yes:
providers are created with &lt;code>POST /backend/v1/providers&lt;/code> and carry a
&lt;code>template.url&lt;/code> — an arbitrary endpoint.)&lt;/em>&lt;/li>
&lt;/ol>
&lt;p>That second one matters because it decides whether &amp;ldquo;agentgateway behind F5&amp;rdquo;
is even possible. It is. Let&amp;rsquo;s walk through all three options.&lt;/p>
&lt;h2 id="first-get-the-product-boundaries-right">First, get the product boundaries right&lt;/h2>
&lt;p>One thing that trips people up: &lt;strong>F5 AI Guardrails is not an F5 XC feature.&lt;/strong>
It&amp;rsquo;s the CalypsoAI platform under F5 branding — its own console, its own
projects and API tokens, delivered as SaaS (&lt;code>us1.calypsoai.app&lt;/code> /
&lt;code>eu1.calypsoai.app&lt;/code>) or self-hosted on Kubernetes/OpenShift. Your XC tenant
gives you the WAAP edge (TLS, WAF, bot defense, DDoS, API protection); the
Guardrails license is separate.&lt;/p>
&lt;p>The three products in play:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Product&lt;/th>
&lt;th>Job&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Edge&lt;/td>
&lt;td>F5 Distributed Cloud HTTP LB&lt;/td>
&lt;td>Public entry: TLS, WAF, bot defense, DDoS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI data plane&lt;/td>
&lt;td>agentgateway&lt;/td>
&lt;td>OpenAI-compatible routing, failover, authn, budgets, MCP/A2A, OTel&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AI security&lt;/td>
&lt;td>F5 AI Guardrails (ex-CalypsoAI)&lt;/td>
&lt;td>Prompt/response scanning: inject, jailbreak, PII, custom scanners&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Guardrails itself supports two consumption modes, and every architecture below
is a composition of them:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Inline&lt;/strong> — Guardrails terminates the request, scans, and forwards to a
configured provider. Speaks its native PromptAPI (&lt;code>POST /backend/v1/prompts&lt;/code>)
&lt;em>or&lt;/em> plain OpenAI chat completions (&lt;code>POST /openai/{provider}/chat/completions&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Out-of-band&lt;/strong> — a pure verdict API. &lt;code>POST /backend/v1/scans&lt;/code> takes
&lt;code>input&lt;/code>, &lt;code>project&lt;/code>, and &lt;code>scanDirection&lt;/code> (&lt;code>request&lt;/code> or &lt;code>response&lt;/code>) and returns
an outcome plus &lt;code>redactedInput&lt;/code>. Nothing gets forwarded; &lt;em>you&lt;/em> stay in charge
of calling the model.&lt;/li>
&lt;/ul>
&lt;h2 id="why-the-gateway-layer-is-non-negotiable">Why the gateway layer is non-negotiable&lt;/h2>
&lt;p>Before the architectures, it&amp;rsquo;s worth being blunt about why agentgateway is in
every one of these diagrams — because &amp;ldquo;can&amp;rsquo;t the apps just call Guardrails
directly?&amp;rdquo; is the first question you&amp;rsquo;ll get in an architecture review. They
can (that&amp;rsquo;s Lab 3 of the F5 workshop), and it doesn&amp;rsquo;t scale. Every other
layer in the stack assumes someone else is governing AI traffic: the XC edge
governs HTTP and doesn&amp;rsquo;t know what a token, a model, or a tool call is;
Guardrails judges &lt;em>content&lt;/em> and doesn&amp;rsquo;t route, failover, meter, or
authenticate your apps. Without a gateway, your &amp;ldquo;AI platform&amp;rdquo; is a pile of
SDK calls nobody owns.&lt;/p>
&lt;p>What the gateway layer uniquely provides:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>One front door instead of N×M integrations.&lt;/strong> Ten apps on three
providers is thirty credential sets and thirty places to touch every time
a model deprecates. Behind one OpenAI-compatible endpoint, providers and
models change without a single app redeploy.&lt;/li>
&lt;li>&lt;strong>Provider keys leave the apps.&lt;/strong> Apps authenticate to &lt;em>you&lt;/em> (JWT, API
key, OAuth); OpenAI/Anthropic credentials exist in exactly one place.
Revoking a team&amp;rsquo;s access takes seconds and doesn&amp;rsquo;t rotate a provider key
across the fleet.&lt;/li>
&lt;li>&lt;strong>Spend control that actually blocks.&lt;/strong> Provider dashboards report last
month; gateway budgets are enforced live — dollar- or token-denominated,
per team or app, returning &lt;code>429&lt;/code> when the money runs out. The edge can
rate-limit requests, but a request can cost $0.001 or $5; only the layer
that parses usage can meter dollars.&lt;/li>
&lt;li>&lt;strong>Failover and model agility.&lt;/strong> Provider outages and deprecations are
&lt;em>when&lt;/em>, not &lt;em>if&lt;/em>. Automatic cross-provider failover and model aliasing
turn every provider incident from an all-hands app change into a config
edit.&lt;/li>
&lt;li>&lt;strong>MCP and A2A governance.&lt;/strong> Agents call tools, and ungoverned MCP servers
are the new shadow IT — every tool an agent can reach is an exfiltration
path. agentgateway federates MCP servers behind auth and RBAC; neither
the edge nor Guardrails addresses this at all.&lt;/li>
&lt;li>&lt;strong>Observability where the semantics live.&lt;/strong> OTel traces with tokens,
cost, model, and latency per request. The edge sees bytes; the guardrail
sees verdicts; only the gateway sees the whole conversation.&lt;/li>
&lt;li>&lt;strong>It&amp;rsquo;s the socket the guardrail plugs into.&lt;/strong> Without a gateway, adopting
Guardrails means every app team writes scan-then-forward code. With one,
guardrails become a &lt;em>policy&lt;/em> — one webhook config, zero app changes
(that&amp;rsquo;s Option C below).&lt;/li>
&lt;/ol>
&lt;p>And the honest trade-offs: it&amp;rsquo;s another hop (single-digit milliseconds —
noise next to seconds of LLM inference) and another component to operate; if
it&amp;rsquo;s down, AI traffic is down (it&amp;rsquo;s a stateless Rust proxy built to run as
HA replicas); the budget/webhook/UI features are in the enterprise tier. The
counterargument to all three is the same: without the gateway you end up
building ad-hoc versions of half these features anyway — badly, in every
app, with no one accountable.&lt;/p>
&lt;p>You wouldn&amp;rsquo;t run web apps without a load balancer or APIs without an API
gateway. Running LLMs and agents without an AI gateway is the same mistake,
except the blast radius is your provider bill, your credentials, and every
tool your agents can touch. The edge protects you from the internet;
Guardrails protects you from the content; &lt;strong>the gateway is what makes AI
traffic governable at all.&lt;/strong>&lt;/p>
&lt;h2 id="option-a--agentgateway-in-front-of-guardrails">Option A — agentgateway in front of Guardrails&lt;/h2>
&lt;p>The zero-code option. Guardrails&amp;rsquo; inline endpoint is OpenAI-compatible, so
agentgateway just treats it as one more LLM backend. F5 makes the final hop to
the provider.&lt;/p>
&lt;div class="mermaid">flowchart LR
 C[&amp;#34;AI apps / agents&amp;lt;br/&amp;gt;(OpenAI SDK)&amp;#34;] --&amp;gt; AGW[&amp;#34;agentgateway&amp;lt;br/&amp;gt;authn · routing · budgets · OTel&amp;#34;]
 AGW --&amp;gt; GR[&amp;#34;F5 AI Guardrails (inline)&amp;lt;br/&amp;gt;/openai/openai-prod/chat/completions&amp;lt;br/&amp;gt;scans prompt + response&amp;#34;]
 GR --&amp;gt; LLM[&amp;#34;LLM provider&amp;lt;br/&amp;gt;(configured in Guardrails)&amp;#34;]
&lt;/div>
&lt;p>The flow, end to end:&lt;/p>
&lt;div class="mermaid">sequenceDiagram
 participant App as AI app
 participant AGW as agentgateway
 participant GR as F5 AI Guardrails
 participant LLM as LLM provider

 App-&amp;gt;&amp;gt;AGW: POST /v1/chat/completions
 AGW-&amp;gt;&amp;gt;AGW: authn, rate limit, model alias, budget
 AGW-&amp;gt;&amp;gt;GR: POST /openai/{provider}/chat/completions
 GR-&amp;gt;&amp;gt;GR: scan prompt (inject, jailbreak, PII...)
 alt prompt blocked
 GR--&amp;gt;&amp;gt;AGW: policy violation
 AGW--&amp;gt;&amp;gt;App: error (logged + traced)
 else prompt clear
 GR-&amp;gt;&amp;gt;LLM: forward (transformed to provider spec)
 LLM--&amp;gt;&amp;gt;GR: completion
 GR-&amp;gt;&amp;gt;GR: scan response
 GR--&amp;gt;&amp;gt;AGW: sanitized completion
 AGW--&amp;gt;&amp;gt;App: completion (+ usage, cost, trace)
 end
&lt;/div>
&lt;p>Configuration is just a custom OpenAI-compatible backend in agentgateway
pointing at the Guardrails host, with the CalypsoAI token as backend auth. The
same mechanics F5 documents for pointing the raw OpenAI SDK at Guardrails
(&lt;code>base_url = &amp;quot;{BASE_URL}/openai/{CONNECTION_NAME}&amp;quot;&lt;/code>) apply — agentgateway is
simply the client.&lt;/p>
&lt;p>&lt;strong>Choose this when&lt;/strong> the security team owns model access end-to-end and you
want the fastest possible integration. &lt;strong>Trade-off:&lt;/strong> the &lt;em>final&lt;/em> provider
choice lives in Guardrails&amp;rsquo; provider config, so agentgateway&amp;rsquo;s multi-provider
failover happens between Guardrails connections rather than directly against
the LLMs.&lt;/p>
&lt;h2 id="option-b--agentgateway-behind-guardrails">Option B — agentgateway behind Guardrails&lt;/h2>
&lt;p>The reverse: Guardrails is the client-facing boundary and delegates the actual
model plumbing to agentgateway. This only works if you can override the
endpoint Guardrails forwards to — and you can. A Guardrails &lt;strong>provider&lt;/strong> is
defined by a request template with a &lt;code>url&lt;/code>, &lt;code>method&lt;/code>, &lt;code>headers&lt;/code>, and body
mapping, so you point that template at agentgateway&amp;rsquo;s OpenAI-compatible
listener:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="err">POST&lt;/span> &lt;span class="err">/backend/v&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="err">/providers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;template&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;request&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://agw.internal.example.com/v1/chat/completions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;headers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;Authorization&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Bearer {{agw_token}}&amp;#34;&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;secrets&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nt">&amp;#34;agw_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;projectId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;guardrails-project&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="mermaid">flowchart LR
 C[&amp;#34;AI apps / agents&amp;#34;] --&amp;gt; GR[&amp;#34;F5 AI Guardrails (inline)&amp;lt;br/&amp;gt;terminates + scans&amp;#34;]
 GR --&amp;gt;|&amp;#34;provider template.url →&amp;#34;| AGW[&amp;#34;agentgateway&amp;#34;]
 AGW --&amp;gt; L1[&amp;#34;OpenAI&amp;#34;]
 AGW --&amp;gt; L2[&amp;#34;Anthropic&amp;#34;]
 AGW --&amp;gt; L3[&amp;#34;vLLM (self-hosted)&amp;#34;]
 AGW --&amp;gt; MCP[&amp;#34;MCP tool servers&amp;#34;]
&lt;/div>
&lt;div class="mermaid">sequenceDiagram
 participant App as AI app
 participant GR as F5 AI Guardrails
 participant AGW as agentgateway
 participant LLM as best provider

 App-&amp;gt;&amp;gt;GR: POST /openai/agentgateway/chat/completions
 GR-&amp;gt;&amp;gt;GR: scan prompt
 GR-&amp;gt;&amp;gt;AGW: forward via provider template.url
 AGW-&amp;gt;&amp;gt;AGW: route, failover, budget, trace
 AGW-&amp;gt;&amp;gt;LLM: provider-native request
 LLM--&amp;gt;&amp;gt;AGW: completion
 AGW--&amp;gt;&amp;gt;GR: OpenAI-format response
 GR-&amp;gt;&amp;gt;GR: scan response
 GR--&amp;gt;&amp;gt;App: sanitized completion
&lt;/div>
&lt;p>&lt;strong>Choose this when&lt;/strong> the security team insists on owning the client-facing
endpoint, but you still want gateway-grade routing/failover/spend control
underneath the guardrail check. &lt;strong>Trade-off:&lt;/strong> the provider-template mechanism
is documented, but chaining it into another gateway isn&amp;rsquo;t an F5-published
pattern — validate streaming pass-through and tool/function-call payloads in a
spike before committing.&lt;/p>
&lt;h2 id="option-c--out-of-band-agentgateway-calls-guardrails-as-a-scanner">Option C — out-of-band: agentgateway calls Guardrails as a scanner&lt;/h2>
&lt;p>My favorite long-term shape. agentgateway stays the single inference path and
invokes Guardrails&amp;rsquo; ScanAPI as a side-scan in both directions, using
agentgateway&amp;rsquo;s &lt;strong>webhook prompt guards&lt;/strong>. Guardrails never proxies anything;
it just renders verdicts. The only code you write is a thin adapter
(~100 lines) that translates agentgateway&amp;rsquo;s webhook contract to
&lt;code>POST /backend/v1/scans&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>Guardrails outcome = flagged → webhook &lt;strong>rejects&lt;/strong> (client gets a 4xx)&lt;/li>
&lt;li>Guardrails returns &lt;code>redactedInput&lt;/code> → webhook &lt;strong>modifies&lt;/strong> (masked content
goes forward)&lt;/li>
&lt;li>otherwise → &lt;strong>allow&lt;/strong>&lt;/li>
&lt;/ul>
&lt;div class="mermaid">flowchart LR
 C[&amp;#34;AI apps / agents&amp;#34;] --&amp;gt; AGW[&amp;#34;agentgateway&amp;#34;]
 AGW -. &amp;#34;request webhook&amp;#34; .-&amp;gt; AD[&amp;#34;guardrails adapter&amp;lt;br/&amp;gt;(~100 lines)&amp;#34;]
 AD -. &amp;#34;POST /backend/v1/scans&amp;lt;br/&amp;gt;scanDirection=request&amp;#34; .-&amp;gt; GR[&amp;#34;F5 AI Guardrails&amp;lt;br/&amp;gt;projects · scanners · audit&amp;#34;]
 AGW --&amp;gt; LLM[&amp;#34;LLM providers&amp;#34;]
 AGW -. &amp;#34;response webhook&amp;#34; .-&amp;gt; AD
&lt;/div>
&lt;div class="mermaid">sequenceDiagram
 participant App as AI app
 participant AGW as agentgateway
 participant AD as adapter
 participant GR as F5 Guardrails
 participant LLM as LLM provider

 App-&amp;gt;&amp;gt;AGW: POST /v1/chat/completions
 AGW-&amp;gt;&amp;gt;AD: promptGuard request webhook
 AD-&amp;gt;&amp;gt;GR: POST /backend/v1/scans (scanDirection=request)
 GR--&amp;gt;&amp;gt;AD: outcome + redactedInput
 alt flagged
 AD--&amp;gt;&amp;gt;AGW: reject
 AGW--&amp;gt;&amp;gt;App: 403 policy violation
 else cleared / redacted
 AD--&amp;gt;&amp;gt;AGW: allow or modify
 AGW-&amp;gt;&amp;gt;LLM: forward
 LLM--&amp;gt;&amp;gt;AGW: completion
 AGW-&amp;gt;&amp;gt;AD: promptGuard response webhook
 AD-&amp;gt;&amp;gt;GR: POST /backend/v1/scans (scanDirection=response)
 GR--&amp;gt;&amp;gt;AD: outcome
 AD--&amp;gt;&amp;gt;AGW: allow / modify / reject
 AGW--&amp;gt;&amp;gt;App: final response
 end
&lt;/div>
&lt;p>Wiring it up on the agentgateway side is a policy targeting your LLM route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-guardrails&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">kind: Service, name: f5-guardrails-adapter, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">response&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">kind: Service, name: f5-guardrails-adapter, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Choose this when&lt;/strong> you want agentgateway to keep full provider control
(failover, streaming straight from the provider, budgets) while the security
team keeps full policy control in the Guardrails console — clean separation of
planes, one hop fewer on the token stream. &lt;strong>Trade-off:&lt;/strong> you own the adapter,
and scanning &lt;em>streamed&lt;/em> responses forces a buffering decision
(scan-on-complete vs. chunked) that you should prototype early. If this
pattern spreads, the natural product evolution is native F5 ScanAPI support in
agentgateway&amp;rsquo;s guardrail family, right next to Bedrock Guardrails and Model
Armor.&lt;/p>
&lt;h2 id="the-full-picture-f5-xc-on-the-edge">The full picture: F5 XC on the edge&lt;/h2>
&lt;p>If you already run F5 Distributed Cloud, the AI stack slots in behind it the
same way your web properties do. XC doesn&amp;rsquo;t host Guardrails — it contributes
the hardened public edge, and an origin pool pointing at agentgateway:&lt;/p>
&lt;div class="mermaid">flowchart TB
 U[&amp;#34;Internet clients / AI apps&amp;#34;] --&amp;gt; XC[&amp;#34;F5 XC HTTP Load Balancer&amp;lt;br/&amp;gt;TLS · WAF · bot defense · DDoS · API protection&amp;#34;]
 XC --&amp;gt;|origin pool| AGW[&amp;#34;agentgateway (K8s or XC CE site)&amp;lt;br/&amp;gt;LLM + MCP + A2A gateway&amp;#34;]
 AGW -. &amp;#34;Option C webhook&amp;#34; .-&amp;gt; AD[&amp;#34;guardrails adapter&amp;#34;]
 AD -.-&amp;gt; GR[&amp;#34;F5 AI Guardrails&amp;lt;br/&amp;gt;SaaS us1/eu1.calypsoai.app or self-hosted&amp;#34;]
 AGW --&amp;gt; P1[&amp;#34;OpenAI&amp;#34;]
 AGW --&amp;gt; P2[&amp;#34;Anthropic&amp;#34;]
 AGW --&amp;gt; P3[&amp;#34;vLLM / self-hosted&amp;#34;]
 AGW --&amp;gt; MCP[&amp;#34;MCP tool servers&amp;#34;]
&lt;/div>
&lt;p>Every layer does the one thing it&amp;rsquo;s best at: XC absorbs the internet,
agentgateway governs AI traffic, Guardrails judges content. And each layer is
independently swappable — which is exactly what you want when this space is
moving as fast as it is.&lt;/p>
&lt;h2 id="choosing-between-them">Choosing between them&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>A: agw → F5&lt;/th>
&lt;th>B: F5 → agw&lt;/th>
&lt;th>C: out-of-band&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Code required&lt;/td>
&lt;td>none&lt;/td>
&lt;td>none&lt;/td>
&lt;td>small adapter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client-facing endpoint&lt;/td>
&lt;td>agentgateway&lt;/td>
&lt;td>Guardrails&lt;/td>
&lt;td>agentgateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Provider routing/failover&lt;/td>
&lt;td>in Guardrails&lt;/td>
&lt;td>in agentgateway&lt;/td>
&lt;td>in agentgateway&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Extra proxy hop on token stream&lt;/td>
&lt;td>yes&lt;/td>
&lt;td>yes&lt;/td>
&lt;td>no (verdicts only)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Streaming risk&lt;/td>
&lt;td>F5-documented&lt;/td>
&lt;td>needs spike&lt;/td>
&lt;td>buffering choice on response scan&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Best for&lt;/td>
&lt;td>fastest PoC&lt;/td>
&lt;td>security-team-owned front door&lt;/td>
&lt;td>production, clean separation of planes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>My recommendation: &lt;strong>prove Option A in an afternoon&lt;/strong> (it&amp;rsquo;s configuration
only), then &lt;strong>build Option C for production&lt;/strong>. Option B is the niche play for
orgs whose security team must terminate the client connection.&lt;/p>
&lt;h2 id="runnable-lab-and-test-results">Runnable lab and test results&lt;/h2>
&lt;p>I put the working kind lab here:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai">github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai&lt;/a>&lt;/strong>&lt;/p>
&lt;p>The lab deploys both recommended paths:&lt;/p>
&lt;ul>
&lt;li>&lt;code>/option-a&lt;/code> routes from agentgateway to the F5 AI Guardrails
OpenAI-compatible inline endpoint.&lt;/li>
&lt;li>&lt;code>/option-c&lt;/code> routes from agentgateway directly to OpenAI, with F5 ScanAPI
called out-of-band from request and response &lt;code>promptGuard&lt;/code> webhooks.&lt;/li>
&lt;/ul>
&lt;p>The setup script creates two intentionally simple custom scanners in F5 AI
Guardrails:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scanner&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Match&lt;/th>
&lt;th>Direction&lt;/th>
&lt;th>Mode&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agw-lab-keyword-codename&lt;/code>&lt;/td>
&lt;td>Keyword&lt;/td>
&lt;td>&lt;code>project-titan&lt;/code>&lt;/td>
&lt;td>Prompts and responses&lt;/td>
&lt;td>Block&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>agw-lab-regex-ssn&lt;/code>&lt;/td>
&lt;td>Regex&lt;/td>
&lt;td>&lt;code>\d{3}-\d{2}-\d{4}&lt;/code>&lt;/td>
&lt;td>Prompts&lt;/td>
&lt;td>Redact&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That gives the test suite something concrete to prove. The smoke test lives in
&lt;code>test.sh&lt;/code> and sends six OpenAI Chat Completions requests through the gateway:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Test&lt;/th>
&lt;th>Route&lt;/th>
&lt;th>Expected result&lt;/th>
&lt;th>What it proves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Option A benign&lt;/td>
&lt;td>&lt;code>/option-a&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>&lt;/td>
&lt;td>agentgateway can reach the F5 inline OpenAI-compatible endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C benign&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>&lt;/td>
&lt;td>agentgateway can reach OpenAI directly while the webhook adapter passes clear prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option A blocked codename&lt;/td>
&lt;td>&lt;code>/option-a&lt;/code>&lt;/td>
&lt;td>&lt;code>400&lt;/code> or &lt;code>403&lt;/code>&lt;/td>
&lt;td>F5 inline scanning blocks the custom &lt;code>project-titan&lt;/code> keyword&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C blocked codename&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>403&lt;/code>&lt;/td>
&lt;td>the request webhook calls ScanAPI and rejects a blocked prompt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C SSN redaction&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>, no raw SSN&lt;/td>
&lt;td>ScanAPI returns &lt;code>redactedInput&lt;/code>, and the adapter replaces the user message before forwarding&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Option C response-phase mask&lt;/td>
&lt;td>&lt;code>/option-c&lt;/code>&lt;/td>
&lt;td>&lt;code>200&lt;/code>, no blocked keyword&lt;/td>
&lt;td>the response webhook scans the assistant output and masks blocked content&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The important design choice is that Option C tests both directions. Request
scanning catches unsafe user input before the model sees it; response scanning
catches unsafe model output before the client sees it.&lt;/p>
&lt;p>This is the traffic flow the tests exercise:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/agw-f5-traffic-flow.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/agw-f5-traffic-flow.gif" alt="Animated traffic flow for test.sh showing Option A inline F5 Guardrails traffic and Option C out-of-band ScanAPI traffic through agentgateway webhooks" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Run it after deployment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/202-agw-f5-ai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup-guardrails.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The passing output from my run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">PASS Option A benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C benign: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option A blocked codename: HTTP 400
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C blocked codename: HTTP 403
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C SSN redaction request completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C redaction did not leak raw SSN
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scan completed: HTTP 200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">PASS Option C response-phase scanner masked blocked output
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The same run shows up in F5 AI Guardrails under &lt;strong>Logs → Prompt history&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/f5-logs-test-output.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-07-02-agentgateway-f5-ai-guardrails-architectures/f5-logs-test-output.png" alt="F5 AI Guardrails prompt history showing blocked project-titan scans and redacted SSN scans from the agentgateway test run" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Those rows are the audit trail that matters in a real rollout:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Blocked / Scan / &lt;code>project-titan&lt;/code>&lt;/strong> — Option C&amp;rsquo;s request webhook called
ScanAPI before the model request and F5 returned a blocking verdict.&lt;/li>
&lt;li>&lt;strong>Redacted / Scan / &lt;code>Repeat back exactly: my ssn is ***********&lt;/code>&lt;/strong> — the
regex scanner caught the SSN pattern and returned redacted content instead of
the raw value.&lt;/li>
&lt;li>&lt;strong>Blocked / Prompt / Genai Azure Openai&lt;/strong> — Option A hit the F5 inline
OpenAI-compatible provider, so F5 logged the proxied prompt path as well as
the scan decision.&lt;/li>
&lt;/ul>
&lt;p>This is the operational difference between &amp;ldquo;the gateway returned the right
status code&amp;rdquo; and &amp;ldquo;security has evidence.&amp;rdquo; The terminal output proves
agentgateway enforced the policy; the Guardrails logs prove the scanner
decision, user, provider path, interaction text, and timestamp were recorded in
the F5 audit plane.&lt;/p>
&lt;p>There is also a fuller harness in &lt;code>run_harness.sh&lt;/code> that reads
&lt;code>harness/cases.yaml&lt;/code>, records latency/status/usage metadata, and writes
&lt;code>harness/results.jsonl&lt;/code>. Use &lt;code>test.sh&lt;/code> when you want a quick operational
answer; use the harness when you want repeatable evidence for a write-up,
demo, or CI job.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.aisecurity.f5.com/">F5 AI Guardrails API docs&lt;/a> — &lt;a href="https://docs.aisecurity.f5.com/api-docs/getting-started-defend.html">getting started with Defend&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/operations/post_scans.html">ScanAPI&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/operations/post_providers.html">providers&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/operations/post_openai_provider_chat_completions.html">OpenAI-compatible endpoint&lt;/a>, &lt;a href="https://docs.aisecurity.f5.com/integrations/proxy.openai-sdk.html">OpenAI SDK proxy integration&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/agentgateway-demos/tree/main/202-agw-f5-ai">Runnable Options A and C lab&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/f5devcentral/f5-ai-security-api-integration-examples">F5 AI security API integration examples (GitHub)&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/latest/">agentgateway docs&lt;/a> — &lt;a href="https://docs.solo.io/agentgateway/latest/llm/providers/custom/">custom providers&lt;/a>, &lt;a href="https://docs.solo.io/agentgateway/latest/llm/guardrails/webhook/">webhook guardrails&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://clouddocs.f5.com/training/community/f5xc-emea-workshop/html/class6/module1/module1.html">F5 XC EMEA workshop, Class 6 Module 1&lt;/a> — Lab 2 (inline), Lab 3 (out-of-band)&lt;/li>
&lt;/ul></content:encoded></item><item><title>Suspend &amp; Resume Stateful Agents: kagent on Agent Substrate with kind</title><link>https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/</link><pubDate>Thu, 25 Jun 2026 10:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-06-25-kagent-agent-substrate-suspend-resume-kind/</guid><description>&lt;h1 id="suspend--resume-stateful-agents-kagent-on-agent-substrate-with-kind">Suspend &amp;amp; Resume Stateful Agents: kagent on Agent Substrate with kind&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>Agent sessions are bursty. A user asks a question, the agent thinks for a few seconds, then the session sits idle for minutes — or hours — waiting on the next turn. Plain Kubernetes handles this badly: an idle pod still books its CPU and memory, and a cold pod takes seconds to come back. Multiply that across thousands of conversations and you&amp;rsquo;re paying for a lot of nothing.&lt;/p>
&lt;p>&lt;strong>Agent Substrate&lt;/strong> flips the model. It decouples the &lt;em>agent session&lt;/em> from the &lt;em>pod&lt;/em>: idle sessions are checkpointed — full RAM and filesystem, via &lt;a href="https://gvisor.dev/">gVisor&lt;/a> — to object storage, the pod returns to a warm pool, and the session resumes &lt;strong>sub-second&lt;/strong> on the next request, exactly where it left off. Think serverless scale-to-zero, but for &lt;em>stateful&lt;/em> agents.&lt;/p>
&lt;p>In this how-to we&amp;rsquo;ll stand the whole thing up on a throwaway &lt;a href="https://kind.sigs.k8s.io/">kind&lt;/a> cluster: &lt;strong>Agent Substrate&lt;/strong>, &lt;strong>kagent&lt;/strong> wired to use it as its execution layer, and a tiny &lt;code>SandboxAgent&lt;/code> running as a gVisor actor. Then we&amp;rsquo;ll chat with it and watch it suspend and resume.&lt;/p>
&lt;blockquote>
&lt;p>This is a standalone, run-it-yourself adaptation of the &lt;a href="https://github.com/sebbycorp/Instruqt-demos/tree/main/01-kagent-agent-substrate-workshop">Agent Substrate with kagent Instruqt workshop&lt;/a>. No Instruqt account required — just a Linux box.&lt;/p>
&lt;/blockquote>
&lt;h2 id="why-this-setup">Why This Setup?&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Serverless economics for stateful agents&lt;/strong> — idle actors cost (almost) nothing; you pay for a small pool of warm workers, not one pod per session.&lt;/li>
&lt;li>&lt;strong>Sub-second resume&lt;/strong> — gVisor checkpoint/restore brings a suspended session back where it left off, no cold-start penalty.&lt;/li>
&lt;li>&lt;strong>Disposable environment&lt;/strong> — the whole thing is one &lt;code>kind&lt;/code> cluster. Break it, delete it, recreate it.&lt;/li>
&lt;li>&lt;strong>kagent-native&lt;/strong> — kagent &lt;code>0.9.7&lt;/code> is the first release that can use substrate as its execution layer, so a declarative agent becomes a substrate actor with one field: &lt;code>platform: substrate&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-substrate-model-in-30-seconds">The Substrate Model in 30 Seconds&lt;/h2>
&lt;p>Agent Substrate decouples &lt;strong>actor&lt;/strong> lifecycle from &lt;strong>pod&lt;/strong> lifecycle:&lt;/p>
&lt;ul>
&lt;li>An &lt;strong>Actor&lt;/strong> is one logical agent session — state &lt;code>RUNNING&lt;/code> or &lt;code>SUSPENDED&lt;/code>.&lt;/li>
&lt;li>A &lt;strong>Worker&lt;/strong> is a pre-warmed pod hosting at most one actor — state &lt;code>IDLE&lt;/code> or &lt;code>BUSY&lt;/code>.&lt;/li>
&lt;li>Idle actors are &lt;strong>suspended&lt;/strong>: gVisor checkpoints their full state to object storage and the worker returns to the pool.&lt;/li>
&lt;li>On the next request the actor is &lt;strong>resumed&lt;/strong> sub-second into a free worker — exactly where it left off.&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Term&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Actor&lt;/strong>&lt;/td>
&lt;td>One logical agent session.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Worker&lt;/strong>&lt;/td>
&lt;td>A pre-warmed pod hosting at most one actor.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>WorkerPool&lt;/strong>&lt;/td>
&lt;td>A set of warm standby worker pods (a CRD).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ActorTemplate&lt;/strong>&lt;/td>
&lt;td>The immutable &amp;ldquo;class&amp;rdquo; an actor is created from (a CRD). Creating one builds a &lt;strong>golden snapshot&lt;/strong> (version 0).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden snapshot&lt;/strong>&lt;/td>
&lt;td>The initial frozen image every new actor is restored from.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Suspend / Resume&lt;/strong>&lt;/td>
&lt;td>Checkpoint actor state to object storage / restore it sub-second.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────────────────── kind cluster (kagent-substrate) ────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ namespace: kagent namespace: ate-system │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────────────┐ ┌──────────────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-controller │ controller.substrate.* │ ate-api-server (scheduling) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-ui │ ───────────────────────▶│ atenet-router (L7 routing) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ SandboxAgent │ │ │ atelet (node supervisor) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ hello-substrate │ │ │ valkey-cluster (state) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ WorkerPool │ │ │ rustfs (snapshots/S3) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-default │ │ └──────────────────────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent Substrate&lt;/strong> (&lt;code>ate-system&lt;/code>)&lt;/td>
&lt;td>Kubernetes-native runtime that maps many stateful &lt;strong>actors&lt;/strong> onto a small pool of pre-warmed &lt;strong>worker&lt;/strong> pods. Idle actors are checkpointed (RAM + filesystem, via gVisor) to object storage and resumed sub-second on demand.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong> (&lt;code>kagent&lt;/code>)&lt;/td>
&lt;td>The agent control plane / runtime, wired to use substrate as its execution layer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong>&lt;/td>
&lt;td>A per-session declarative agent that runs as a gVisor &lt;strong>actor&lt;/strong> on the substrate.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>You need a &lt;strong>Linux&lt;/strong> machine (or a Linux VM) with &lt;strong>at least ~8 vCPUs and 16 GB RAM&lt;/strong> — the substrate control plane runs a 6-node valkey cluster plus several other components, so a tiny machine will struggle.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>macOS / Windows note:&lt;/strong> gVisor checkpoint/restore relies on Linux kernel features. Run this on Linux — a cloud VM such as a GCP &lt;code>n1-standard-8&lt;/code>, an EC2 &lt;code>m5.2xlarge&lt;/code>, or equivalent works well. Docker Desktop on Mac/Windows runs containers in a Linux VM and may not support the gVisor checkpointing path reliably.&lt;/p>
&lt;/blockquote>
&lt;p>Tools and the versions this guide is pinned to:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Version&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Docker&lt;/td>
&lt;td>any recent&lt;/td>
&lt;td>&lt;code>kind&lt;/code> runs the cluster in containers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kind&lt;/code>&lt;/td>
&lt;td>&lt;code>v0.27.0&lt;/code>&lt;/td>
&lt;td>Kubernetes-in-Docker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Node image&lt;/td>
&lt;td>&lt;code>kindest/node:v1.32.2&lt;/code>&lt;/td>
&lt;td>&lt;strong>k8s 1.31+ required&lt;/strong> — substrate CRDs use CEL &lt;code>format&lt;/code> rules&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kubectl&lt;/code>&lt;/td>
&lt;td>matches cluster&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>helm&lt;/code>&lt;/td>
&lt;td>v3&lt;/td>
&lt;td>installs the charts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>grpcurl&lt;/code>&lt;/td>
&lt;td>&lt;code>v1.9.1&lt;/code>&lt;/td>
&lt;td>drive the substrate &lt;code>ate-api&lt;/code> directly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OpenAI API key&lt;/td>
&lt;td>—&lt;/td>
&lt;td>the agent makes real LLM calls&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Pinned component versions used throughout:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Version&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Agent Substrate&lt;/td>
&lt;td>&lt;code>0.0.6&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kagent (OSS)&lt;/td>
&lt;td>&lt;code>0.9.7&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gVisor actor image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.6&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>⚠️ &lt;strong>Why the node image matters:&lt;/strong> the substrate &lt;code>0.0.6&lt;/code> CRDs use CEL validation rules that call the &lt;code>format&lt;/code> library (&lt;code>format.dns1123Label&lt;/code> / &lt;code>dns1123Subdomain&lt;/code>), added in Kubernetes &lt;strong>1.31&lt;/strong>. An older &lt;code>kind&lt;/code> default node image will reject the CRDs. Pin &lt;code>kindest/node:v1.32.2&lt;/code> or newer.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-0--install-tooling">Step 0 — Install Tooling&lt;/h2>
&lt;p>Run these on a Linux host. Skip any tool you already have at a compatible version.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># kubectl&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSLo /usr/local/bin/kubectl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://dl.k8s.io/release/&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://dl.k8s.io/release/stable.txt&lt;span class="k">)&lt;/span>&lt;span class="s2">/bin/linux/amd64/kubectl&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x /usr/local/bin/kubectl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># helm v3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># kind v0.27.0 (pin it — older kind ships an older default node image)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSLo /usr/local/bin/kind &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x /usr/local/bin/kind
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># grpcurl v1.9.1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL &lt;span class="s2">&amp;#34;https://github.com/fullstorydev/grpcurl/releases/download/v1.9.1/grpcurl_1.9.1_linux_x86_64.tar.gz&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tar -xz -C /usr/local/bin grpcurl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x /usr/local/bin/grpcurl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Make sure Docker is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo systemctl start docker &lt;span class="c1"># if applicable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker info &amp;gt;/dev/null &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;docker OK&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Export your OpenAI key (the agent makes real LLM calls):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-...your-key...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;key set (len=&lt;/span>&lt;span class="si">${#&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OPENAI_API_KEY is empty!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>⚠️ &lt;strong>Footgun:&lt;/strong> don&amp;rsquo;t inline the assignment on the helm line like &lt;code>OPENAI_API_KEY=... helm ... --set ...=&amp;quot;${OPENAI_API_KEY}&amp;quot;&lt;/code> — the variable is expanded &lt;em>before&lt;/em> the assignment runs and you&amp;rsquo;ll silently pass an empty string. Export it first, as above.&lt;/p>
&lt;/blockquote>
&lt;p>Pin a few env vars for convenience:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="o">=&lt;/span>kagent-substrate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.0.6
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.9.7
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-1--create-the-kind-cluster">Step 1 — Create the kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --image kindest/node:v1.32.2 --wait 120s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cluster-info
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see one node in &lt;code>Ready&lt;/code> state. Confirm your tools resolve:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm version --short
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcurl --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2--install-agent-substrate">Step 2 — Install Agent Substrate&lt;/h2>
&lt;p>Everything lands in the &lt;code>ate-system&lt;/code> namespace. The substrate &lt;code>0.0.6&lt;/code> chart defaults to &lt;strong>JWT auth&lt;/strong> (ServiceAccount tokens), so a stock &lt;code>kind&lt;/code> cluster works — no feature gates, no custom kind config.&lt;/p>
&lt;p>Install the CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.0.6 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --create-namespace --wait
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the control plane (this pulls several images and starts the valkey cluster — give it a few minutes):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.0.6 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --wait --timeout 10m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Watch it come up:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n ate-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait until you see all of these &lt;code>Running&lt;/code> (a couple of init Jobs will show &lt;code>Completed&lt;/code>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pod&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>ate-api-server&lt;/code>&lt;/td>
&lt;td>Control plane: actor lifecycle, scheduling, suspend/resume. Bypasses kube-scheduler.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ate-controller&lt;/code>&lt;/td>
&lt;td>Reconciles &lt;code>WorkerPool&lt;/code> + &lt;code>ActorTemplate&lt;/code> CRDs into Deployments.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atelet&lt;/code> (DaemonSet)&lt;/td>
&lt;td>Node supervisor: pulls images, manages sandbox lifecycle, talks to object storage.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atenet-router&lt;/code>&lt;/td>
&lt;td>Envoy L7 router; resolves which worker serves each request.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>valkey-cluster-0&lt;/code>..&lt;code>-5&lt;/code>&lt;/td>
&lt;td>State store: actor/worker records and locks.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>rustfs&lt;/code>&lt;/td>
&lt;td>In-cluster S3-compatible object storage holding snapshot images.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Inspect the CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get crd &lt;span class="p">|&lt;/span> grep ate.dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get workerpools.ate.dev -A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>There are no WorkerPools yet — kagent creates one in the next step.&lt;/p>
&lt;h2 id="step-3--install-kagent-wired-to-substrate">Step 3 — Install kagent, Wired to Substrate&lt;/h2>
&lt;p>kagent is the agent control plane; substrate is the execution layer &lt;em>underneath&lt;/em> it. kagent &lt;code>0.9.7&lt;/code> is the first release with substrate support.&lt;/p>
&lt;blockquote>
&lt;p>⚠️ &lt;strong>Order matters:&lt;/strong> the kagent controller &lt;strong>hard-fails (crash-loops)&lt;/strong> if the substrate API isn&amp;rsquo;t reachable at startup — which is why substrate had to be installed first.&lt;/p>
&lt;/blockquote>
&lt;p>Install the kagent CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.9.7 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent --create-namespace --wait
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install kagent with substrate enabled:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.9.7 --namespace kagent --timeout 10m --wait &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKey&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiEndpoint&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dns:///api.ate-system.svc:443&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiInsecure&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.atenetRouterURL&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://atenet-router.ate-system.svc:80&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiTokenFile&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/run/secrets/tokens/ate-api/token&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.namespace&lt;span class="o">=&lt;/span>kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.create&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.replicas&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.ateomImage&lt;span class="o">=&lt;/span>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.6
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What the substrate flags mean:&lt;/p>
&lt;ul>
&lt;li>&lt;code>controller.substrate.enabled=true&lt;/code> — turn on the integration&lt;/li>
&lt;li>&lt;code>controller.substrate.ateApiEndpoint&lt;/code> — where the substrate control plane lives&lt;/li>
&lt;li>&lt;code>controller.substrate.atenetRouterURL&lt;/code> — the substrate request router&lt;/li>
&lt;li>&lt;code>controller.substrate.defaultWorkerPool.*&lt;/code> — the pool agents land on by default&lt;/li>
&lt;li>&lt;code>substrateWorkerPool.create=true&lt;/code> + &lt;code>.replicas=1&lt;/code> — create one warm worker&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>If helm times out while the controller waits on its database (it restarts a couple of times during cold start), wait for it manually and continue:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> deploy/kagent-controller -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Available --timeout&lt;span class="o">=&lt;/span>10m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/blockquote>
&lt;p>Verify the WorkerPool and the integration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get workerpools.ate.dev -A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see &lt;code>kagent/kagent-default&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl run substrate-status-check -n kagent --rm -i --restart&lt;span class="o">=&lt;/span>Never &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --image&lt;span class="o">=&lt;/span>curlimages/curl:8.10.1 -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://kagent-controller:8083/api/substrate/status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The response should include &lt;code>&amp;quot;enabled&amp;quot;: true&lt;/code>.&lt;/p>
&lt;h2 id="step-4--deploy-a-sandboxagent">Step 4 — Deploy a SandboxAgent&lt;/h2>
&lt;p>A kagent &lt;code>SandboxAgent&lt;/code> with &lt;code>platform: substrate&lt;/code> is a per-session declarative agent that runs as a substrate &lt;strong>actor&lt;/strong>. Creating one generates an &lt;code>ActorTemplate&lt;/code>, which triggers the &lt;strong>golden snapshot&lt;/strong> — the version-0 frozen image every session is restored from. The first snapshot takes ~60–90s.&lt;/p>
&lt;blockquote>
&lt;p>The runtime must be &lt;strong>Go&lt;/strong> — Python ADK isn&amp;rsquo;t compatible with gVisor checkpointing.&lt;/p>
&lt;/blockquote>
&lt;p>Write the manifest:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &amp;gt; hello-substrate.yaml &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;YAML&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: kagent.dev/v1alpha2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: SandboxAgent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: hello-substrate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: kagent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: Declarative
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> description: Tiny declarative agent running inside a substrate actor
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> declarative:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> runtime: go
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> modelConfig: default-model-config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> systemMessage: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> You are a friendly assistant living inside an Agent Substrate sandbox.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> When asked who you are, say &amp;#34;I am hello-substrate, a Go ADK declarative
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> agent running inside a gVisor actor.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> platform: substrate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> substrate:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> workerPoolRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: kagent-default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">YAML&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apply it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f hello-substrate.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for it to be Ready (first golden snapshot takes ~60–90s):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> sandboxagent/hello-substrate -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Ready --timeout&lt;span class="o">=&lt;/span>5m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Inspect the generated substrate resources:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get sandboxagent -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get actortemplates.ate.dev -A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>kagent generated an &lt;code>ActorTemplate&lt;/code> for your agent (owned by the SandboxAgent).&lt;/p>
&lt;h2 id="step-5--chat-then-watch-suspend--resume">Step 5 — Chat, Then Watch Suspend / Resume&lt;/h2>
&lt;p>When you chat with &lt;code>hello-substrate&lt;/code>, a per-session gVisor &lt;strong>actor&lt;/strong> is restored from the golden snapshot, runs the LLM call, and snapshots itself back to object storage — returning the worker to the pool. Between requests the actor sits &lt;strong>SUSPENDED&lt;/strong>; on the next request it&amp;rsquo;s &lt;strong>RESUMED&lt;/strong> sub-second.&lt;/p>
&lt;h3 id="5a-open-the-kagent-ui">5a. Open the kagent UI&lt;/h3>
&lt;p>Port-forward the UI and open it in your browser:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward svc/kagent-ui 8080:8080 --address 0.0.0.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;a href="http://localhost:8080">http://localhost:8080&lt;/a> (or &lt;code>http://&amp;lt;vm-ip&amp;gt;:8080&lt;/code>). Pick &lt;code>kagent/hello-substrate&lt;/code> from the Agents list and send:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>What are you, and where are you running? Answer in one sentence.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>You should get back something like:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>I am hello-substrate, a Go ADK declarative agent running inside a gVisor actor.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>Then open the &lt;strong>Substrate&lt;/strong> page (&lt;code>/substrate&lt;/code>) in the UI to see the worker pool and actors.&lt;/p>
&lt;h3 id="5b-drive-the-actor-lifecycle-from-the-cli">5b. Drive the actor lifecycle from the CLI&lt;/h3>
&lt;p>In another terminal, port-forward the substrate API in the background:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n ate-system svc/api 18443:443 &amp;gt;/tmp/pf-api.log 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sleep &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mint a short-lived token the kagent controller&amp;rsquo;s identity can use:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl create token kagent-controller -n kagent --audience&lt;span class="o">=&lt;/span>api.ate-system.svc --duration&lt;span class="o">=&lt;/span>15m&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>List the actors — note your actor&amp;rsquo;s id and its status (it will be &lt;code>SUSPENDED&lt;/code> between requests):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListActors
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Resume the actor explicitly — watch it flip to &lt;code>RUNNING&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;actor_id&amp;#34;:&amp;#34;&amp;lt;ACTOR_ID&amp;gt;&amp;#34;}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ResumeActor
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Re-run &lt;code>ListActors&lt;/code> to confirm the state change. That round trip — &lt;code>SUSPENDED&lt;/code> in object storage, &lt;code>RESUMED&lt;/code> into a worker on demand — is the whole point of substrate.&lt;/p>
&lt;h2 id="recap">Recap&lt;/h2>
&lt;p>You now have, on a single &lt;code>kind&lt;/code> cluster:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Agent Substrate&lt;/strong> (&lt;code>ate-system&lt;/code>) — control plane (&lt;code>ate-api-server&lt;/code>), router (&lt;code>atenet-router&lt;/code>), node supervisor (&lt;code>atelet&lt;/code>), state (&lt;code>valkey&lt;/code>), snapshots (&lt;code>rustfs&lt;/code>).&lt;/li>
&lt;li>&lt;strong>kagent&lt;/strong> (&lt;code>kagent&lt;/code>) — wired to substrate with a default &lt;code>WorkerPool&lt;/code>.&lt;/li>
&lt;li>A &lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong> running as a per-session gVisor actor, suspended to object storage between requests and resumed sub-second on demand.&lt;/li>
&lt;/ul>
&lt;h3 id="scaling-the-workerpool">Scaling the WorkerPool&lt;/h3>
&lt;p>One worker serves many declarative sessions sequentially because each session releases its slot the moment it snapshots back. To run overlapping sessions or long-lived agents, scale the pool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale workerpool kagent-default -n kagent --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="whats-next-beyond-this-guide">What&amp;rsquo;s Next (Beyond This Guide)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;code>AgentHarness&lt;/code> (&lt;code>runtime: substrate&lt;/code>)&lt;/strong> — long-lived runtimes run as substrate actors reached through a kagent gateway. These need an object-storage bucket (e.g. &lt;code>gs://...&lt;/code>) and don&amp;rsquo;t auto-suspend, so each pins a worker slot.&lt;/li>
&lt;li>&lt;strong>Identity&lt;/strong> — substrate can mint per-actor JWTs and certs for mTLS.&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — substrate exposes metrics for activation latency and worker pool use.&lt;/li>
&lt;/ul>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Symptom&lt;/th>
&lt;th>Likely cause / fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Substrate CRDs rejected on install&lt;/td>
&lt;td>Node image too old — recreate the cluster with &lt;code>kindest/node:v1.32.2&lt;/code> or newer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-controller&lt;/code> crash-loops&lt;/td>
&lt;td>Substrate API not reachable at startup — confirm &lt;code>ate-system&lt;/code> pods are all &lt;code>Running&lt;/code> before installing kagent.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kagent install silently &amp;ldquo;works&amp;rdquo; but the agent fails on chat&lt;/td>
&lt;td>&lt;code>OPENAI_API_KEY&lt;/code> was empty — re-export it (don&amp;rsquo;t inline the assignment on the helm line) and re-run the kagent &lt;code>helm upgrade&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Helm install times out on kagent&lt;/td>
&lt;td>The controller restarts during cold start; run &lt;code>kubectl wait deploy/kagent-controller -n kagent --for=condition=Available --timeout=10m&lt;/code> and continue.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ListActors&lt;/code> returns auth errors&lt;/td>
&lt;td>Token expired (15m TTL) — re-mint with the &lt;code>kubectl create token&lt;/code> command.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Everything was fine yesterday, broken today&lt;/td>
&lt;td>Substrate&amp;rsquo;s in-cluster TLS certs expire after ~24h on idle clusters — best run in a single sitting. Recreate the cluster if needed.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Note:&lt;/strong> Agent Substrate is &lt;strong>very early / pre-1.0&lt;/strong> — APIs will change and it&amp;rsquo;s not production-ready. gVisor checkpoint/restore requires a live Linux host to test.&lt;/p>
&lt;/blockquote>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name kagent-substrate
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="try-it-yourself">Try It Yourself&lt;/h2>
&lt;p>The full standalone how-to (with the exact manifests used here) lives in the &lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">kagent-demos repo&lt;/a>. Prefer a guided, browser-based lab with no setup? Run the &lt;a href="https://github.com/sebbycorp/Instruqt-demos/tree/main/01-kagent-agent-substrate-workshop">Agent Substrate with kagent Instruqt workshop&lt;/a> instead — same path, six bite-sized challenges.&lt;/p>
&lt;p>Suspend-and-resume is the feature that finally makes per-session stateful agents affordable at scale. Spin it up, chat with &lt;code>hello-substrate&lt;/code>, and watch a worker serve far more sessions than it has slots for.&lt;/p></description><content:encoded>&lt;h1 id="suspend--resume-stateful-agents-kagent-on-agent-substrate-with-kind">Suspend &amp;amp; Resume Stateful Agents: kagent on Agent Substrate with kind&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>Agent sessions are bursty. A user asks a question, the agent thinks for a few seconds, then the session sits idle for minutes — or hours — waiting on the next turn. Plain Kubernetes handles this badly: an idle pod still books its CPU and memory, and a cold pod takes seconds to come back. Multiply that across thousands of conversations and you&amp;rsquo;re paying for a lot of nothing.&lt;/p>
&lt;p>&lt;strong>Agent Substrate&lt;/strong> flips the model. It decouples the &lt;em>agent session&lt;/em> from the &lt;em>pod&lt;/em>: idle sessions are checkpointed — full RAM and filesystem, via &lt;a href="https://gvisor.dev/">gVisor&lt;/a> — to object storage, the pod returns to a warm pool, and the session resumes &lt;strong>sub-second&lt;/strong> on the next request, exactly where it left off. Think serverless scale-to-zero, but for &lt;em>stateful&lt;/em> agents.&lt;/p>
&lt;p>In this how-to we&amp;rsquo;ll stand the whole thing up on a throwaway &lt;a href="https://kind.sigs.k8s.io/">kind&lt;/a> cluster: &lt;strong>Agent Substrate&lt;/strong>, &lt;strong>kagent&lt;/strong> wired to use it as its execution layer, and a tiny &lt;code>SandboxAgent&lt;/code> running as a gVisor actor. Then we&amp;rsquo;ll chat with it and watch it suspend and resume.&lt;/p>
&lt;blockquote>
&lt;p>This is a standalone, run-it-yourself adaptation of the &lt;a href="https://github.com/sebbycorp/Instruqt-demos/tree/main/01-kagent-agent-substrate-workshop">Agent Substrate with kagent Instruqt workshop&lt;/a>. No Instruqt account required — just a Linux box.&lt;/p>
&lt;/blockquote>
&lt;h2 id="why-this-setup">Why This Setup?&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Serverless economics for stateful agents&lt;/strong> — idle actors cost (almost) nothing; you pay for a small pool of warm workers, not one pod per session.&lt;/li>
&lt;li>&lt;strong>Sub-second resume&lt;/strong> — gVisor checkpoint/restore brings a suspended session back where it left off, no cold-start penalty.&lt;/li>
&lt;li>&lt;strong>Disposable environment&lt;/strong> — the whole thing is one &lt;code>kind&lt;/code> cluster. Break it, delete it, recreate it.&lt;/li>
&lt;li>&lt;strong>kagent-native&lt;/strong> — kagent &lt;code>0.9.7&lt;/code> is the first release that can use substrate as its execution layer, so a declarative agent becomes a substrate actor with one field: &lt;code>platform: substrate&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="the-substrate-model-in-30-seconds">The Substrate Model in 30 Seconds&lt;/h2>
&lt;p>Agent Substrate decouples &lt;strong>actor&lt;/strong> lifecycle from &lt;strong>pod&lt;/strong> lifecycle:&lt;/p>
&lt;ul>
&lt;li>An &lt;strong>Actor&lt;/strong> is one logical agent session — state &lt;code>RUNNING&lt;/code> or &lt;code>SUSPENDED&lt;/code>.&lt;/li>
&lt;li>A &lt;strong>Worker&lt;/strong> is a pre-warmed pod hosting at most one actor — state &lt;code>IDLE&lt;/code> or &lt;code>BUSY&lt;/code>.&lt;/li>
&lt;li>Idle actors are &lt;strong>suspended&lt;/strong>: gVisor checkpoints their full state to object storage and the worker returns to the pool.&lt;/li>
&lt;li>On the next request the actor is &lt;strong>resumed&lt;/strong> sub-second into a free worker — exactly where it left off.&lt;/li>
&lt;/ul>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Term&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Actor&lt;/strong>&lt;/td>
&lt;td>One logical agent session.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Worker&lt;/strong>&lt;/td>
&lt;td>A pre-warmed pod hosting at most one actor.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>WorkerPool&lt;/strong>&lt;/td>
&lt;td>A set of warm standby worker pods (a CRD).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ActorTemplate&lt;/strong>&lt;/td>
&lt;td>The immutable &amp;ldquo;class&amp;rdquo; an actor is created from (a CRD). Creating one builds a &lt;strong>golden snapshot&lt;/strong> (version 0).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden snapshot&lt;/strong>&lt;/td>
&lt;td>The initial frozen image every new actor is restored from.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Suspend / Resume&lt;/strong>&lt;/td>
&lt;td>Checkpoint actor state to object storage / restore it sub-second.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────────────────── kind cluster (kagent-substrate) ────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ namespace: kagent namespace: ate-system │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────────────┐ ┌──────────────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-controller │ controller.substrate.* │ ate-api-server (scheduling) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-ui │ ───────────────────────▶│ atenet-router (L7 routing) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ SandboxAgent │ │ │ atelet (node supervisor) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ hello-substrate │ │ │ valkey-cluster (state) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ WorkerPool │ │ │ rustfs (snapshots/S3) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent-default │ │ └──────────────────────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────────────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent Substrate&lt;/strong> (&lt;code>ate-system&lt;/code>)&lt;/td>
&lt;td>Kubernetes-native runtime that maps many stateful &lt;strong>actors&lt;/strong> onto a small pool of pre-warmed &lt;strong>worker&lt;/strong> pods. Idle actors are checkpointed (RAM + filesystem, via gVisor) to object storage and resumed sub-second on demand.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong> (&lt;code>kagent&lt;/code>)&lt;/td>
&lt;td>The agent control plane / runtime, wired to use substrate as its execution layer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong>&lt;/td>
&lt;td>A per-session declarative agent that runs as a gVisor &lt;strong>actor&lt;/strong> on the substrate.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>You need a &lt;strong>Linux&lt;/strong> machine (or a Linux VM) with &lt;strong>at least ~8 vCPUs and 16 GB RAM&lt;/strong> — the substrate control plane runs a 6-node valkey cluster plus several other components, so a tiny machine will struggle.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>macOS / Windows note:&lt;/strong> gVisor checkpoint/restore relies on Linux kernel features. Run this on Linux — a cloud VM such as a GCP &lt;code>n1-standard-8&lt;/code>, an EC2 &lt;code>m5.2xlarge&lt;/code>, or equivalent works well. Docker Desktop on Mac/Windows runs containers in a Linux VM and may not support the gVisor checkpointing path reliably.&lt;/p>
&lt;/blockquote>
&lt;p>Tools and the versions this guide is pinned to:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Version&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Docker&lt;/td>
&lt;td>any recent&lt;/td>
&lt;td>&lt;code>kind&lt;/code> runs the cluster in containers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kind&lt;/code>&lt;/td>
&lt;td>&lt;code>v0.27.0&lt;/code>&lt;/td>
&lt;td>Kubernetes-in-Docker&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Node image&lt;/td>
&lt;td>&lt;code>kindest/node:v1.32.2&lt;/code>&lt;/td>
&lt;td>&lt;strong>k8s 1.31+ required&lt;/strong> — substrate CRDs use CEL &lt;code>format&lt;/code> rules&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kubectl&lt;/code>&lt;/td>
&lt;td>matches cluster&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>helm&lt;/code>&lt;/td>
&lt;td>v3&lt;/td>
&lt;td>installs the charts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>grpcurl&lt;/code>&lt;/td>
&lt;td>&lt;code>v1.9.1&lt;/code>&lt;/td>
&lt;td>drive the substrate &lt;code>ate-api&lt;/code> directly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OpenAI API key&lt;/td>
&lt;td>—&lt;/td>
&lt;td>the agent makes real LLM calls&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Pinned component versions used throughout:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Version&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Agent Substrate&lt;/td>
&lt;td>&lt;code>0.0.6&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kagent (OSS)&lt;/td>
&lt;td>&lt;code>0.9.7&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gVisor actor image&lt;/td>
&lt;td>&lt;code>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.6&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>⚠️ &lt;strong>Why the node image matters:&lt;/strong> the substrate &lt;code>0.0.6&lt;/code> CRDs use CEL validation rules that call the &lt;code>format&lt;/code> library (&lt;code>format.dns1123Label&lt;/code> / &lt;code>dns1123Subdomain&lt;/code>), added in Kubernetes &lt;strong>1.31&lt;/strong>. An older &lt;code>kind&lt;/code> default node image will reject the CRDs. Pin &lt;code>kindest/node:v1.32.2&lt;/code> or newer.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-0--install-tooling">Step 0 — Install Tooling&lt;/h2>
&lt;p>Run these on a Linux host. Skip any tool you already have at a compatible version.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># kubectl&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSLo /usr/local/bin/kubectl &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://dl.k8s.io/release/&lt;/span>&lt;span class="k">$(&lt;/span>curl -fsSL https://dl.k8s.io/release/stable.txt&lt;span class="k">)&lt;/span>&lt;span class="s2">/bin/linux/amd64/kubectl&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x /usr/local/bin/kubectl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># helm v3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># kind v0.27.0 (pin it — older kind ships an older default node image)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSLo /usr/local/bin/kind &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x /usr/local/bin/kind
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># grpcurl v1.9.1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -fsSL &lt;span class="s2">&amp;#34;https://github.com/fullstorydev/grpcurl/releases/download/v1.9.1/grpcurl_1.9.1_linux_x86_64.tar.gz&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> tar -xz -C /usr/local/bin grpcurl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x /usr/local/bin/grpcurl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Make sure Docker is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sudo systemctl start docker &lt;span class="c1"># if applicable&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker info &amp;gt;/dev/null &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;docker OK&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Export your OpenAI key (the agent makes real LLM calls):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-...your-key...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;key set (len=&lt;/span>&lt;span class="si">${#&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OPENAI_API_KEY is empty!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>⚠️ &lt;strong>Footgun:&lt;/strong> don&amp;rsquo;t inline the assignment on the helm line like &lt;code>OPENAI_API_KEY=... helm ... --set ...=&amp;quot;${OPENAI_API_KEY}&amp;quot;&lt;/code> — the variable is expanded &lt;em>before&lt;/em> the assignment runs and you&amp;rsquo;ll silently pass an empty string. Export it first, as above.&lt;/p>
&lt;/blockquote>
&lt;p>Pin a few env vars for convenience:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="o">=&lt;/span>kagent-substrate
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SUBSTRATE_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.0.6
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">KAGENT_VERSION&lt;/span>&lt;span class="o">=&lt;/span>0.9.7
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-1--create-the-kind-cluster">Step 1 — Create the kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">KIND_CLUSTER&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --image kindest/node:v1.32.2 --wait 120s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cluster-info
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see one node in &lt;code>Ready&lt;/code> state. Confirm your tools resolve:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind version
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm version --short
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcurl --version
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2--install-agent-substrate">Step 2 — Install Agent Substrate&lt;/h2>
&lt;p>Everything lands in the &lt;code>ate-system&lt;/code> namespace. The substrate &lt;code>0.0.6&lt;/code> chart defaults to &lt;strong>JWT auth&lt;/strong> (ServiceAccount tokens), so a stock &lt;code>kind&lt;/code> cluster works — no feature gates, no custom kind config.&lt;/p>
&lt;p>Install the CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.0.6 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --create-namespace --wait
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the control plane (this pulls several images and starts the valkey cluster — give it a few minutes):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/substrate/helm/substrate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.0.6 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace ate-system --wait --timeout 10m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Watch it come up:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n ate-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait until you see all of these &lt;code>Running&lt;/code> (a couple of init Jobs will show &lt;code>Completed&lt;/code>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pod&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>ate-api-server&lt;/code>&lt;/td>
&lt;td>Control plane: actor lifecycle, scheduling, suspend/resume. Bypasses kube-scheduler.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ate-controller&lt;/code>&lt;/td>
&lt;td>Reconciles &lt;code>WorkerPool&lt;/code> + &lt;code>ActorTemplate&lt;/code> CRDs into Deployments.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atelet&lt;/code> (DaemonSet)&lt;/td>
&lt;td>Node supervisor: pulls images, manages sandbox lifecycle, talks to object storage.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>atenet-router&lt;/code>&lt;/td>
&lt;td>Envoy L7 router; resolves which worker serves each request.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>valkey-cluster-0&lt;/code>..&lt;code>-5&lt;/code>&lt;/td>
&lt;td>State store: actor/worker records and locks.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>rustfs&lt;/code>&lt;/td>
&lt;td>In-cluster S3-compatible object storage holding snapshot images.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Inspect the CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get crd &lt;span class="p">|&lt;/span> grep ate.dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get workerpools.ate.dev -A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>There are no WorkerPools yet — kagent creates one in the next step.&lt;/p>
&lt;h2 id="step-3--install-kagent-wired-to-substrate">Step 3 — Install kagent, Wired to Substrate&lt;/h2>
&lt;p>kagent is the agent control plane; substrate is the execution layer &lt;em>underneath&lt;/em> it. kagent &lt;code>0.9.7&lt;/code> is the first release with substrate support.&lt;/p>
&lt;blockquote>
&lt;p>⚠️ &lt;strong>Order matters:&lt;/strong> the kagent controller &lt;strong>hard-fails (crash-loops)&lt;/strong> if the substrate API isn&amp;rsquo;t reachable at startup — which is why substrate had to be installed first.&lt;/p>
&lt;/blockquote>
&lt;p>Install the kagent CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.9.7 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent --create-namespace --wait
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install kagent with substrate enabled:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 0.9.7 --namespace kagent --timeout 10m --wait &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKey&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiEndpoint&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;dns:///api.ate-system.svc:443&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiInsecure&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.atenetRouterURL&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://atenet-router.ate-system.svc:80&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.ateApiTokenFile&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/var/run/secrets/tokens/ate-api/token&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.namespace&lt;span class="o">=&lt;/span>kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.substrate.defaultWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.create&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.name&lt;span class="o">=&lt;/span>kagent-default &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.replicas&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set substrateWorkerPool.ateomImage&lt;span class="o">=&lt;/span>ghcr.io/kagent-dev/substrate/ateom-gvisor:v0.0.6
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What the substrate flags mean:&lt;/p>
&lt;ul>
&lt;li>&lt;code>controller.substrate.enabled=true&lt;/code> — turn on the integration&lt;/li>
&lt;li>&lt;code>controller.substrate.ateApiEndpoint&lt;/code> — where the substrate control plane lives&lt;/li>
&lt;li>&lt;code>controller.substrate.atenetRouterURL&lt;/code> — the substrate request router&lt;/li>
&lt;li>&lt;code>controller.substrate.defaultWorkerPool.*&lt;/code> — the pool agents land on by default&lt;/li>
&lt;li>&lt;code>substrateWorkerPool.create=true&lt;/code> + &lt;code>.replicas=1&lt;/code> — create one warm worker&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>If helm times out while the controller waits on its database (it restarts a couple of times during cold start), wait for it manually and continue:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> deploy/kagent-controller -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Available --timeout&lt;span class="o">=&lt;/span>10m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/blockquote>
&lt;p>Verify the WorkerPool and the integration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get workerpools.ate.dev -A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see &lt;code>kagent/kagent-default&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl run substrate-status-check -n kagent --rm -i --restart&lt;span class="o">=&lt;/span>Never &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --image&lt;span class="o">=&lt;/span>curlimages/curl:8.10.1 -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://kagent-controller:8083/api/substrate/status
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The response should include &lt;code>&amp;quot;enabled&amp;quot;: true&lt;/code>.&lt;/p>
&lt;h2 id="step-4--deploy-a-sandboxagent">Step 4 — Deploy a SandboxAgent&lt;/h2>
&lt;p>A kagent &lt;code>SandboxAgent&lt;/code> with &lt;code>platform: substrate&lt;/code> is a per-session declarative agent that runs as a substrate &lt;strong>actor&lt;/strong>. Creating one generates an &lt;code>ActorTemplate&lt;/code>, which triggers the &lt;strong>golden snapshot&lt;/strong> — the version-0 frozen image every session is restored from. The first snapshot takes ~60–90s.&lt;/p>
&lt;blockquote>
&lt;p>The runtime must be &lt;strong>Go&lt;/strong> — Python ADK isn&amp;rsquo;t compatible with gVisor checkpointing.&lt;/p>
&lt;/blockquote>
&lt;p>Write the manifest:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &amp;gt; hello-substrate.yaml &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;YAML&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: kagent.dev/v1alpha2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: SandboxAgent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: hello-substrate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: kagent
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: Declarative
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> description: Tiny declarative agent running inside a substrate actor
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> declarative:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> runtime: go
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> modelConfig: default-model-config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> systemMessage: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> You are a friendly assistant living inside an Agent Substrate sandbox.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> When asked who you are, say &amp;#34;I am hello-substrate, a Go ADK declarative
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> agent running inside a gVisor actor.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> platform: substrate
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> substrate:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> workerPoolRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: kagent-default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">YAML&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apply it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f hello-substrate.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for it to be Ready (first golden snapshot takes ~60–90s):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> sandboxagent/hello-substrate -n kagent --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Ready --timeout&lt;span class="o">=&lt;/span>5m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Inspect the generated substrate resources:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get sandboxagent -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get actortemplates.ate.dev -A
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>kagent generated an &lt;code>ActorTemplate&lt;/code> for your agent (owned by the SandboxAgent).&lt;/p>
&lt;h2 id="step-5--chat-then-watch-suspend--resume">Step 5 — Chat, Then Watch Suspend / Resume&lt;/h2>
&lt;p>When you chat with &lt;code>hello-substrate&lt;/code>, a per-session gVisor &lt;strong>actor&lt;/strong> is restored from the golden snapshot, runs the LLM call, and snapshots itself back to object storage — returning the worker to the pool. Between requests the actor sits &lt;strong>SUSPENDED&lt;/strong>; on the next request it&amp;rsquo;s &lt;strong>RESUMED&lt;/strong> sub-second.&lt;/p>
&lt;h3 id="5a-open-the-kagent-ui">5a. Open the kagent UI&lt;/h3>
&lt;p>Port-forward the UI and open it in your browser:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl -n kagent port-forward svc/kagent-ui 8080:8080 --address 0.0.0.0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;a href="http://localhost:8080">http://localhost:8080&lt;/a> (or &lt;code>http://&amp;lt;vm-ip&amp;gt;:8080&lt;/code>). Pick &lt;code>kagent/hello-substrate&lt;/code> from the Agents list and send:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>What are you, and where are you running? Answer in one sentence.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>You should get back something like:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>I am hello-substrate, a Go ADK declarative agent running inside a gVisor actor.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>Then open the &lt;strong>Substrate&lt;/strong> page (&lt;code>/substrate&lt;/code>) in the UI to see the worker pool and actors.&lt;/p>
&lt;h3 id="5b-drive-the-actor-lifecycle-from-the-cli">5b. Drive the actor lifecycle from the CLI&lt;/h3>
&lt;p>In another terminal, port-forward the substrate API in the background:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n ate-system svc/api 18443:443 &amp;gt;/tmp/pf-api.log 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sleep &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mint a short-lived token the kagent controller&amp;rsquo;s identity can use:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">TOKEN&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl create token kagent-controller -n kagent --audience&lt;span class="o">=&lt;/span>api.ate-system.svc --duration&lt;span class="o">=&lt;/span>15m&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>List the actors — note your actor&amp;rsquo;s id and its status (it will be &lt;code>SUSPENDED&lt;/code> between requests):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -d &lt;span class="s1">&amp;#39;{}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ListActors
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Resume the actor explicitly — watch it flip to &lt;code>RUNNING&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">grpcurl -insecure -H &lt;span class="s2">&amp;#34;authorization: Bearer &lt;/span>&lt;span class="nv">$TOKEN&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;actor_id&amp;#34;:&amp;#34;&amp;lt;ACTOR_ID&amp;gt;&amp;#34;}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> localhost:18443 ateapi.Control/ResumeActor
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Re-run &lt;code>ListActors&lt;/code> to confirm the state change. That round trip — &lt;code>SUSPENDED&lt;/code> in object storage, &lt;code>RESUMED&lt;/code> into a worker on demand — is the whole point of substrate.&lt;/p>
&lt;h2 id="recap">Recap&lt;/h2>
&lt;p>You now have, on a single &lt;code>kind&lt;/code> cluster:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Agent Substrate&lt;/strong> (&lt;code>ate-system&lt;/code>) — control plane (&lt;code>ate-api-server&lt;/code>), router (&lt;code>atenet-router&lt;/code>), node supervisor (&lt;code>atelet&lt;/code>), state (&lt;code>valkey&lt;/code>), snapshots (&lt;code>rustfs&lt;/code>).&lt;/li>
&lt;li>&lt;strong>kagent&lt;/strong> (&lt;code>kagent&lt;/code>) — wired to substrate with a default &lt;code>WorkerPool&lt;/code>.&lt;/li>
&lt;li>A &lt;strong>&lt;code>SandboxAgent&lt;/code>&lt;/strong> running as a per-session gVisor actor, suspended to object storage between requests and resumed sub-second on demand.&lt;/li>
&lt;/ul>
&lt;h3 id="scaling-the-workerpool">Scaling the WorkerPool&lt;/h3>
&lt;p>One worker serves many declarative sessions sequentially because each session releases its slot the moment it snapshots back. To run overlapping sessions or long-lived agents, scale the pool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale workerpool kagent-default -n kagent --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="whats-next-beyond-this-guide">What&amp;rsquo;s Next (Beyond This Guide)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;code>AgentHarness&lt;/code> (&lt;code>runtime: substrate&lt;/code>)&lt;/strong> — long-lived runtimes run as substrate actors reached through a kagent gateway. These need an object-storage bucket (e.g. &lt;code>gs://...&lt;/code>) and don&amp;rsquo;t auto-suspend, so each pins a worker slot.&lt;/li>
&lt;li>&lt;strong>Identity&lt;/strong> — substrate can mint per-actor JWTs and certs for mTLS.&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — substrate exposes metrics for activation latency and worker pool use.&lt;/li>
&lt;/ul>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Symptom&lt;/th>
&lt;th>Likely cause / fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Substrate CRDs rejected on install&lt;/td>
&lt;td>Node image too old — recreate the cluster with &lt;code>kindest/node:v1.32.2&lt;/code> or newer.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>kagent-controller&lt;/code> crash-loops&lt;/td>
&lt;td>Substrate API not reachable at startup — confirm &lt;code>ate-system&lt;/code> pods are all &lt;code>Running&lt;/code> before installing kagent.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>kagent install silently &amp;ldquo;works&amp;rdquo; but the agent fails on chat&lt;/td>
&lt;td>&lt;code>OPENAI_API_KEY&lt;/code> was empty — re-export it (don&amp;rsquo;t inline the assignment on the helm line) and re-run the kagent &lt;code>helm upgrade&lt;/code>.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Helm install times out on kagent&lt;/td>
&lt;td>The controller restarts during cold start; run &lt;code>kubectl wait deploy/kagent-controller -n kagent --for=condition=Available --timeout=10m&lt;/code> and continue.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ListActors&lt;/code> returns auth errors&lt;/td>
&lt;td>Token expired (15m TTL) — re-mint with the &lt;code>kubectl create token&lt;/code> command.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Everything was fine yesterday, broken today&lt;/td>
&lt;td>Substrate&amp;rsquo;s in-cluster TLS certs expire after ~24h on idle clusters — best run in a single sitting. Recreate the cluster if needed.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Note:&lt;/strong> Agent Substrate is &lt;strong>very early / pre-1.0&lt;/strong> — APIs will change and it&amp;rsquo;s not production-ready. gVisor checkpoint/restore requires a live Linux host to test.&lt;/p>
&lt;/blockquote>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name kagent-substrate
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="try-it-yourself">Try It Yourself&lt;/h2>
&lt;p>The full standalone how-to (with the exact manifests used here) lives in the &lt;a href="https://github.com/sebbycorp/kagent-demos/tree/main/01-kagent-agent-substrate">kagent-demos repo&lt;/a>. Prefer a guided, browser-based lab with no setup? Run the &lt;a href="https://github.com/sebbycorp/Instruqt-demos/tree/main/01-kagent-agent-substrate-workshop">Agent Substrate with kagent Instruqt workshop&lt;/a> instead — same path, six bite-sized challenges.&lt;/p>
&lt;p>Suspend-and-resume is the feature that finally makes per-session stateful agents affordable at scale. Spin it up, chat with &lt;code>hello-substrate&lt;/code>, and watch a worker serve far more sessions than it has slots for.&lt;/p></content:encoded></item><item><title>AgentGateway Standalone — Cost &amp; Tokenomics Dashboard Demo</title><link>https://maniak.io/articles/2026-06-24-agentgateway-standalone-cost-tokenomics-dashboard-demo/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-24-agentgateway-standalone-cost-tokenomics-dashboard-demo/</guid><description>&lt;p>Every time I show someone &lt;a href="https://agentgateway.dev">AgentGateway&lt;/a>&amp;rsquo;s &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> dashboards, the same problem comes up: a fresh install has &lt;em>no data&lt;/em>. The dashboard is empty until you&amp;rsquo;ve sent real traffic through it, which means you either wait days for usage to accumulate or you script a load generator and pay for thousands of throwaway API calls just to make some charts light up.&lt;/p>
&lt;p>The &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">&lt;code>00-standalone-latest&lt;/code>&lt;/a> demo solves that. It spins up AgentGateway standalone in a single Docker container with a &lt;strong>pre-populated&lt;/strong> cost database — 5,000 simulated requests spanning 7 days, each priced against a real per-model cost catalog — so the dashboard is fully alive the moment it boots. No waiting, no burned tokens.&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">sebbycorp/agentgateway-demos / 00-standalone-latest&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;strong>TL;DR:&lt;/strong> &lt;code>export OPENAI_API_KEY=… &amp;amp;&amp;amp; ./setup.sh&lt;/code>, then open &lt;code>http://localhost:15000/ui/&lt;/code>. You get a working AgentGateway with a costs dashboard that already has a week of fleet traffic in it — and the LLM endpoint is live on &lt;code>:4000&lt;/code> so you can add your own real requests on top.&lt;/p>
&lt;h2 id="what-you-actually-get">What you actually get&lt;/h2>
&lt;p>The demo is deliberately small — five files do all the work:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>File&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>setup.sh&lt;/code>&lt;/td>
&lt;td>One-shot installer: preflight checks, generates mock data, writes config, launches the container&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config.yaml&lt;/code>&lt;/td>
&lt;td>AgentGateway config — admin UI, SQLite database, model catalog, OpenAI routes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>base-costs.json&lt;/code>&lt;/td>
&lt;td>Per-model pricing rates (OpenAI, Anthropic, Bedrock, Gemini, Mistral, DeepSeek…) so every request gets a real dollar cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen-mock-logs.py&lt;/code>&lt;/td>
&lt;td>Generates realistic fleet traffic and writes it into AGW&amp;rsquo;s &lt;code>request_logs&lt;/code> schema&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>destroy.sh&lt;/code>&lt;/td>
&lt;td>Tears the whole thing down&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The trick that makes it work: the &lt;strong>mock generator writes to the exact same &lt;code>request_logs&lt;/code> table that AgentGateway&amp;rsquo;s dashboard reads from&lt;/strong>, and the model catalog (&lt;code>base-costs.json&lt;/code>) prices every row. So from the dashboard&amp;rsquo;s point of view, the seeded data is indistinguishable from real traffic.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>You need very little:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Docker&lt;/strong> installed and running&lt;/li>
&lt;li>&lt;strong>&lt;code>curl&lt;/code>&lt;/strong> (the script downloads the mock-data generator)&lt;/li>
&lt;li>&lt;strong>Python ≥ 3.11&lt;/strong> or &lt;a href="https://github.com/astral-sh/uv">&lt;code>uv&lt;/code>&lt;/a> to run the generator&lt;/li>
&lt;li>An &lt;strong>&lt;code>OPENAI_API_KEY&lt;/code>&lt;/strong> — required because the live LLM route on &lt;code>:4000&lt;/code> proxies to OpenAI. (The &lt;em>dashboard&lt;/em> data is mock; the &lt;em>live endpoint&lt;/em> is real.)&lt;/li>
&lt;/ul>
&lt;h2 id="run-it">Run it&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/00-standalone-latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the whole thing. &lt;code>setup.sh&lt;/code> walks through:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Preflight&lt;/strong> — checks for Docker + a running daemon, &lt;code>curl&lt;/code>, a Python runner, and &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Fetch the generator&lt;/strong> — downloads &lt;code>gen-mock-logs.py&lt;/code> if it isn&amp;rsquo;t already local.&lt;/li>
&lt;li>&lt;strong>Generate mock data&lt;/strong> — creates a SQLite DB with &lt;strong>5,000 requests across 7 days&lt;/strong> (both configurable, see below) using AGW&amp;rsquo;s &lt;code>request_logs&lt;/code> schema.&lt;/li>
&lt;li>&lt;strong>Write config&lt;/strong> — emits the &lt;code>config.yaml&lt;/code> pointing the admin UI at that database and loading the cost catalog.&lt;/li>
&lt;li>&lt;strong>Launch&lt;/strong> — pulls the image, creates a named volume, seeds the DB, and starts the container.&lt;/li>
&lt;/ol>
&lt;p>When it finishes, open:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">http://localhost:15000/ui/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and head to the &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> sections. They&amp;rsquo;re already full.&lt;/p>
&lt;h3 id="tuning-the-seed">Tuning the seed&lt;/h3>
&lt;p>Three environment variables let you change the shape of the seeded data before you run &lt;code>setup.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">VERSION&lt;/span>&lt;span class="o">=&lt;/span>v1.3.1 &lt;span class="se">\ &lt;/span> &lt;span class="c1"># AgentGateway image tag&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REQUESTS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">20000&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># number of mock requests (default 5000)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">DAYS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">30&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># days to spread them across (default 7)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Bump &lt;code>REQUESTS&lt;/code> and &lt;code>DAYS&lt;/code> if you want to demo what a busier fleet or a longer reporting window looks like.&lt;/p>
&lt;h2 id="whats-under-the-hood-configyaml">What&amp;rsquo;s under the hood: config.yaml&lt;/h2>
&lt;p>The generated config is worth a look because it shows how cost tracking is wired:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0:15000&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># admin UI + dashboards (reachable from host)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sqlite:///data/data.db&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># /data is the mounted ./data dir in the container&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/base-costs.json &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-model rates so every request is priced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;OPTIONS&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/gpt-4.1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># fallback: cheaper nano model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1-nano&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two things to call out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>modelCatalog&lt;/code>&lt;/strong> is what turns raw token counts into dollars. &lt;code>base-costs.json&lt;/code> carries input/output/cache rates for dozens of models across OpenAI, Anthropic, Bedrock, Gemini, Mistral, DeepSeek and more — including tiered pricing for large-context models. Every request in the dashboard is costed against it.&lt;/li>
&lt;li>&lt;strong>The &lt;code>openai/*&lt;/code> fallback route&lt;/strong> quietly downshifts anything that doesn&amp;rsquo;t match a named model to the cheaper &lt;code>gpt-4.1-nano&lt;/code> — a nice pattern for keeping unbudgeted traffic from hitting your most expensive model.&lt;/li>
&lt;/ul>
&lt;p>The container is launched roughly like this (the script handles it for you):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name agw-cost-demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --user 0:0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 4000:4000 -p 15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e OPENAI_API_KEY &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v config.yaml:/config.yaml &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v base-costs.json:/base-costs.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v agw-cost-demo-data:/data &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.3.1 -f /config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Port&lt;/th>
&lt;th>What it serves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>15000&lt;/strong>&lt;/td>
&lt;td>Admin UI — the Costs &amp;amp; Analytics dashboards&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4000&lt;/strong>&lt;/td>
&lt;td>Live LLM endpoint (OpenAI-compatible &lt;code>/v1/chat/completions&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="add-real-traffic-on-top">Add real traffic on top&lt;/h2>
&lt;p>Because &lt;code>:4000&lt;/code> is a live OpenAI-compatible endpoint, you can fire real requests at it and watch them land in the same dashboard alongside the mock data:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;openai/gpt-4.1&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Say hello in one sentence.&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Refresh the dashboard and your call shows up — priced against the same catalog as the seeded rows. This is the best way to convince a skeptical teammate that the cost numbers are real: send a couple of calls and watch the spend tick up.&lt;/p>
&lt;h2 id="tear-it-down">Tear it down&lt;/h2>
&lt;p>When you&amp;rsquo;re done, the demo cleans up after itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./destroy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or do it by hand:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker rm -f agw-cost-demo &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> docker volume rm agw-cost-demo-data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No leftover containers, no stray volumes.&lt;/p>
&lt;h2 id="why-this-demo-is-useful">Why this demo is useful&lt;/h2>
&lt;p>Standalone AgentGateway is the fastest way to understand what the gateway &lt;em>sees&lt;/em> about your LLM spend — which models cost what, where the tokens go, how a fallback route changes the bill. But &amp;ldquo;fastest&amp;rdquo; still normally means &amp;ldquo;after you&amp;rsquo;ve generated enough traffic to have something to look at.&amp;rdquo; This demo collapses that to a single command by &lt;strong>separating the dashboard data from the live path&lt;/strong>: mock data makes the charts immediately meaningful, and the real &lt;code>:4000&lt;/code> route lets you prove the pricing is genuine whenever you want.&lt;/p>
&lt;p>If you&amp;rsquo;ve read my earlier posts on &lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">tool-mode token economics&lt;/a> or &lt;a href="2026-06-22-headroom-agentgateway-mcp-token-stacking.md">stacking Headroom on top of AGW&lt;/a>, this is the dashboard those experiments report into — now you can stand it up in two minutes and explore it yourself.&lt;/p>
&lt;p>👉 Grab it here: &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">sebbycorp/agentgateway-demos / 00-standalone-latest&lt;/a>&lt;/strong>&lt;/p></description><content:encoded>&lt;p>Every time I show someone &lt;a href="https://agentgateway.dev">AgentGateway&lt;/a>&amp;rsquo;s &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> dashboards, the same problem comes up: a fresh install has &lt;em>no data&lt;/em>. The dashboard is empty until you&amp;rsquo;ve sent real traffic through it, which means you either wait days for usage to accumulate or you script a load generator and pay for thousands of throwaway API calls just to make some charts light up.&lt;/p>
&lt;p>The &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">&lt;code>00-standalone-latest&lt;/code>&lt;/a> demo solves that. It spins up AgentGateway standalone in a single Docker container with a &lt;strong>pre-populated&lt;/strong> cost database — 5,000 simulated requests spanning 7 days, each priced against a real per-model cost catalog — so the dashboard is fully alive the moment it boots. No waiting, no burned tokens.&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">sebbycorp/agentgateway-demos / 00-standalone-latest&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;strong>TL;DR:&lt;/strong> &lt;code>export OPENAI_API_KEY=… &amp;amp;&amp;amp; ./setup.sh&lt;/code>, then open &lt;code>http://localhost:15000/ui/&lt;/code>. You get a working AgentGateway with a costs dashboard that already has a week of fleet traffic in it — and the LLM endpoint is live on &lt;code>:4000&lt;/code> so you can add your own real requests on top.&lt;/p>
&lt;h2 id="what-you-actually-get">What you actually get&lt;/h2>
&lt;p>The demo is deliberately small — five files do all the work:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>File&lt;/th>
&lt;th>Role&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>setup.sh&lt;/code>&lt;/td>
&lt;td>One-shot installer: preflight checks, generates mock data, writes config, launches the container&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>config.yaml&lt;/code>&lt;/td>
&lt;td>AgentGateway config — admin UI, SQLite database, model catalog, OpenAI routes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>base-costs.json&lt;/code>&lt;/td>
&lt;td>Per-model pricing rates (OpenAI, Anthropic, Bedrock, Gemini, Mistral, DeepSeek…) so every request gets a real dollar cost&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen-mock-logs.py&lt;/code>&lt;/td>
&lt;td>Generates realistic fleet traffic and writes it into AGW&amp;rsquo;s &lt;code>request_logs&lt;/code> schema&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>destroy.sh&lt;/code>&lt;/td>
&lt;td>Tears the whole thing down&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The trick that makes it work: the &lt;strong>mock generator writes to the exact same &lt;code>request_logs&lt;/code> table that AgentGateway&amp;rsquo;s dashboard reads from&lt;/strong>, and the model catalog (&lt;code>base-costs.json&lt;/code>) prices every row. So from the dashboard&amp;rsquo;s point of view, the seeded data is indistinguishable from real traffic.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>You need very little:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Docker&lt;/strong> installed and running&lt;/li>
&lt;li>&lt;strong>&lt;code>curl&lt;/code>&lt;/strong> (the script downloads the mock-data generator)&lt;/li>
&lt;li>&lt;strong>Python ≥ 3.11&lt;/strong> or &lt;a href="https://github.com/astral-sh/uv">&lt;code>uv&lt;/code>&lt;/a> to run the generator&lt;/li>
&lt;li>An &lt;strong>&lt;code>OPENAI_API_KEY&lt;/code>&lt;/strong> — required because the live LLM route on &lt;code>:4000&lt;/code> proxies to OpenAI. (The &lt;em>dashboard&lt;/em> data is mock; the &lt;em>live endpoint&lt;/em> is real.)&lt;/li>
&lt;/ul>
&lt;h2 id="run-it">Run it&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/00-standalone-latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the whole thing. &lt;code>setup.sh&lt;/code> walks through:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Preflight&lt;/strong> — checks for Docker + a running daemon, &lt;code>curl&lt;/code>, a Python runner, and &lt;code>OPENAI_API_KEY&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Fetch the generator&lt;/strong> — downloads &lt;code>gen-mock-logs.py&lt;/code> if it isn&amp;rsquo;t already local.&lt;/li>
&lt;li>&lt;strong>Generate mock data&lt;/strong> — creates a SQLite DB with &lt;strong>5,000 requests across 7 days&lt;/strong> (both configurable, see below) using AGW&amp;rsquo;s &lt;code>request_logs&lt;/code> schema.&lt;/li>
&lt;li>&lt;strong>Write config&lt;/strong> — emits the &lt;code>config.yaml&lt;/code> pointing the admin UI at that database and loading the cost catalog.&lt;/li>
&lt;li>&lt;strong>Launch&lt;/strong> — pulls the image, creates a named volume, seeds the DB, and starts the container.&lt;/li>
&lt;/ol>
&lt;p>When it finishes, open:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">http://localhost:15000/ui/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and head to the &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> sections. They&amp;rsquo;re already full.&lt;/p>
&lt;h3 id="tuning-the-seed">Tuning the seed&lt;/h3>
&lt;p>Three environment variables let you change the shape of the seeded data before you run &lt;code>setup.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">VERSION&lt;/span>&lt;span class="o">=&lt;/span>v1.3.1 &lt;span class="se">\ &lt;/span> &lt;span class="c1"># AgentGateway image tag&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REQUESTS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">20000&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># number of mock requests (default 5000)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">DAYS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">30&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># days to spread them across (default 7)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Bump &lt;code>REQUESTS&lt;/code> and &lt;code>DAYS&lt;/code> if you want to demo what a busier fleet or a longer reporting window looks like.&lt;/p>
&lt;h2 id="whats-under-the-hood-configyaml">What&amp;rsquo;s under the hood: config.yaml&lt;/h2>
&lt;p>The generated config is worth a look because it shows how cost tracking is wired:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0:15000&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># admin UI + dashboards (reachable from host)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sqlite:///data/data.db&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># /data is the mounted ./data dir in the container&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/base-costs.json &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-model rates so every request is priced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;OPTIONS&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/gpt-4.1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># fallback: cheaper nano model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1-nano&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two things to call out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>modelCatalog&lt;/code>&lt;/strong> is what turns raw token counts into dollars. &lt;code>base-costs.json&lt;/code> carries input/output/cache rates for dozens of models across OpenAI, Anthropic, Bedrock, Gemini, Mistral, DeepSeek and more — including tiered pricing for large-context models. Every request in the dashboard is costed against it.&lt;/li>
&lt;li>&lt;strong>The &lt;code>openai/*&lt;/code> fallback route&lt;/strong> quietly downshifts anything that doesn&amp;rsquo;t match a named model to the cheaper &lt;code>gpt-4.1-nano&lt;/code> — a nice pattern for keeping unbudgeted traffic from hitting your most expensive model.&lt;/li>
&lt;/ul>
&lt;p>The container is launched roughly like this (the script handles it for you):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name agw-cost-demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --user 0:0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 4000:4000 -p 15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e OPENAI_API_KEY &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v config.yaml:/config.yaml &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v base-costs.json:/base-costs.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v agw-cost-demo-data:/data &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.3.1 -f /config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Port&lt;/th>
&lt;th>What it serves&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>15000&lt;/strong>&lt;/td>
&lt;td>Admin UI — the Costs &amp;amp; Analytics dashboards&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>4000&lt;/strong>&lt;/td>
&lt;td>Live LLM endpoint (OpenAI-compatible &lt;code>/v1/chat/completions&lt;/code>)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="add-real-traffic-on-top">Add real traffic on top&lt;/h2>
&lt;p>Because &lt;code>:4000&lt;/code> is a live OpenAI-compatible endpoint, you can fire real requests at it and watch them land in the same dashboard alongside the mock data:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;openai/gpt-4.1&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Say hello in one sentence.&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Refresh the dashboard and your call shows up — priced against the same catalog as the seeded rows. This is the best way to convince a skeptical teammate that the cost numbers are real: send a couple of calls and watch the spend tick up.&lt;/p>
&lt;h2 id="tear-it-down">Tear it down&lt;/h2>
&lt;p>When you&amp;rsquo;re done, the demo cleans up after itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./destroy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>or do it by hand:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker rm -f agw-cost-demo &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> docker volume rm agw-cost-demo-data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No leftover containers, no stray volumes.&lt;/p>
&lt;h2 id="why-this-demo-is-useful">Why this demo is useful&lt;/h2>
&lt;p>Standalone AgentGateway is the fastest way to understand what the gateway &lt;em>sees&lt;/em> about your LLM spend — which models cost what, where the tokens go, how a fallback route changes the bill. But &amp;ldquo;fastest&amp;rdquo; still normally means &amp;ldquo;after you&amp;rsquo;ve generated enough traffic to have something to look at.&amp;rdquo; This demo collapses that to a single command by &lt;strong>separating the dashboard data from the live path&lt;/strong>: mock data makes the charts immediately meaningful, and the real &lt;code>:4000&lt;/code> route lets you prove the pricing is genuine whenever you want.&lt;/p>
&lt;p>If you&amp;rsquo;ve read my earlier posts on &lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">tool-mode token economics&lt;/a> or &lt;a href="2026-06-22-headroom-agentgateway-mcp-token-stacking.md">stacking Headroom on top of AGW&lt;/a>, this is the dashboard those experiments report into — now you can stand it up in two minutes and explore it yourself.&lt;/p>
&lt;p>👉 Grab it here: &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">sebbycorp/agentgateway-demos / 00-standalone-latest&lt;/a>&lt;/strong>&lt;/p></content:encoded></item><item><title>agentgateway Standalone: A Cost &amp; Tokenomics Dashboard in One Command</title><link>https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/</link><pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>You&amp;rsquo;re routing LLM traffic through a gateway. But do you actually know what it &lt;em>costs&lt;/em>? Not the rough monthly invoice from your provider — the real breakdown. Which model burned the most tokens last night? Which user is driving 80% of your spend? Which provider is quietly eating your budget?&lt;/p>
&lt;p>agentgateway answers those questions out of the box. Every request that flows through the proxy is priced against a per-model rate catalog and surfaced in a built-in &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> dashboard. No external observability stack, no Prometheus, no Grafana — just the standalone binary.&lt;/p>
&lt;p>This guide gets you from zero to a fully populated tokenomics dashboard in a single command. We&amp;rsquo;ll use a Docker-based demo that seeds 5,000 simulated requests across 7 days, so the dashboard has something interesting to show you the moment it boots — then we&amp;rsquo;ll send real traffic through it and watch it get priced live.&lt;/p>
&lt;h2 id="why-this-matters">Why This Matters&lt;/h2>
&lt;p>Cost visibility is the FinOps story for AI. As soon as more than one team, agent, or app starts calling LLMs through shared infrastructure, &amp;ldquo;what did this cost and who spent it?&amp;rdquo; becomes a board-level question. agentgateway answers it at the &lt;strong>gateway layer&lt;/strong>, which means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Per-model pricing&lt;/strong> — every request is priced against a rate catalog (input, output, and cache token rates), so dollars show up next to tokens automatically.&lt;/li>
&lt;li>&lt;strong>Group by anything&lt;/strong> — slice spend and tokens by model, provider, user, group, or user agent (Cursor, Claude Code, openai-python, etc.).&lt;/li>
&lt;li>&lt;strong>Zero application changes&lt;/strong> — your apps just point at the gateway. The accounting happens in the proxy, not in your code.&lt;/li>
&lt;li>&lt;strong>One binary&lt;/strong> — the dashboard ships inside agentgateway. There&amp;rsquo;s no separate metrics pipeline to stand up for basic cost visibility.&lt;/li>
&lt;/ul>
&lt;h2 id="what-youll-build">What You&amp;rsquo;ll Build&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> ┌──────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────┐ │ agentgateway │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Your apps / │ /v1/chat/ │ ┌────────────────────────┐ │ ┌────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ agents /curl │───completions──────▶│ │ LLM proxy (port 4000) │──┼─────▶│ OpenAI │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────┘ │ └───────────┬────────────┘ │ └────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ priced per │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ model catalog │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌───────────▼────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────┐ localhost:15000 │ │ Admin UI + Dashboard │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Your browser│────────────────────▶│ │ Costs / Analytics │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────┘ │ └───────────┬────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌───────▼───────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ SQLite data.db │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ request_logs │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └───────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway proxies LLM traffic on port &lt;code>4000&lt;/code> and serves its admin UI and dashboards on port &lt;code>15000&lt;/code>. Every request is written to a SQLite database (&lt;code>data.db&lt;/code>) and priced using a model catalog (&lt;code>base-costs.json&lt;/code>). The mock generator writes to the same &lt;code>request_logs&lt;/code> schema, which is why the dashboard is populated before you send a single real request.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-started/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;code>curl&lt;/code>&lt;/li>
&lt;li>An OpenAI API key&lt;/li>
&lt;li>Python 3.11+ (or &lt;a href="https://docs.astral.sh/uv/">&lt;code>uv&lt;/code>&lt;/a>) — used by the mock-data generator&lt;/li>
&lt;/ul>
&lt;h2 id="quick-start-one-command">Quick Start: One Command&lt;/h2>
&lt;p>Clone the demo and run the setup script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/00-standalone-latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Open &lt;strong>&lt;code>http://localhost:15000/ui/&lt;/code>&lt;/strong> and head to the &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> sections — they&amp;rsquo;re already full of data.&lt;/p>
&lt;h3 id="what-the-script-does">What the script does&lt;/h3>
&lt;p>&lt;code>setup.sh&lt;/code> is a single-command bootstrap. Under the hood it:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Runs preflight checks&lt;/strong> — verifies Docker is installed and the daemon is running, &lt;code>curl&lt;/code> is available, &lt;code>OPENAI_API_KEY&lt;/code> is set, and that &lt;code>uv&lt;/code> or Python 3.11+ is present.&lt;/li>
&lt;li>&lt;strong>Downloads the mock-data generator&lt;/strong> (&lt;code>gen-mock-logs.py&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Generates a SQLite database&lt;/strong> with simulated fleet traffic:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gen-mock-logs.py --replace --requests &lt;span class="m">5000&lt;/span> --days &lt;span class="m">7&lt;/span> -o data/data.db
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>Writes &lt;code>config.yaml&lt;/code>&lt;/strong> pointing the gateway at the database and the model rate catalog.&lt;/li>
&lt;li>&lt;strong>Pulls the agentgateway image&lt;/strong> (&lt;code>cr.agentgateway.dev/agentgateway:v1.3.1&lt;/code>) and removes any previous demo container.&lt;/li>
&lt;li>&lt;strong>Seeds a named Docker volume&lt;/strong> (&lt;code>agw-cost-demo-data&lt;/code>) with the generated database.&lt;/li>
&lt;li>&lt;strong>Launches the container&lt;/strong>, binding ports to loopback only:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">127.0.0.1:4000:4000 # LLM proxy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">127.0.0.1:15000:15000 # admin UI + dashboards
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>&lt;strong>Why loopback only?&lt;/strong> The proxy port carries your API credentials. Binding to &lt;code>127.0.0.1&lt;/code> keeps the demo off your network. Don&amp;rsquo;t expose these ports without locking down auth and CORS first.&lt;/p>
&lt;/blockquote>
&lt;h3 id="tuning-the-dataset">Tuning the dataset&lt;/h3>
&lt;p>Want a bigger or smaller demo dataset? Override the &lt;code>REQUESTS&lt;/code> and &lt;code>DAYS&lt;/code> environment variables before running setup:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REQUESTS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">20000&lt;/span> &lt;span class="nv">DAYS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">30&lt;/span> ./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="exploring-the-dashboard">Exploring the Dashboard&lt;/h2>
&lt;p>Open &lt;code>http://localhost:15000/ui/&lt;/code> and click &lt;strong>Analytics&lt;/strong>. By default it shows total traffic over the last 24 hours — token volume per hour with a running tally of cost, tokens, and calls.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-total.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-total.png" alt="agentgateway Analytics dashboard showing total token traffic over 24 hours, with cost, tokens, and call counts" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The real power is in &lt;strong>Group by&lt;/strong>. Switch it to &lt;strong>Provider&lt;/strong> and the same traffic splits out by backend — here OpenAI dominates with ~13.3M tokens, followed by Anthropic, Google, and Bedrock. The breakdown table underneath ranks every provider by token consumption.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-provider.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-provider.png" alt="agentgateway Analytics grouped by provider, showing OpenAI, Anthropic, Google, and Bedrock token usage" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Switch &lt;strong>Group by&lt;/strong> to &lt;strong>User&lt;/strong> and you get per-person accounting — exactly the view you need when you&amp;rsquo;re trying to figure out who&amp;rsquo;s driving spend. Each bar in the time series is stacked by user, and the breakdown ranks them by tokens consumed.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-user.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-user.png" alt="agentgateway Analytics grouped by user, showing per-person token consumption" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>You can group by &lt;strong>Model&lt;/strong>, &lt;strong>Provider&lt;/strong>, &lt;strong>User&lt;/strong>, &lt;strong>Group&lt;/strong>, or &lt;strong>User agent&lt;/strong> (Cursor, Claude Code, openai-python, codex, bifrost, and more), and switch the &lt;strong>Measure&lt;/strong> between tokens and cost. The &lt;strong>Costs&lt;/strong> page focuses the same data on dollars, and &lt;strong>Export&lt;/strong> lets you pull the underlying numbers out for reporting.&lt;/p>
&lt;h2 id="how-it-works-the-generated-config">How It Works: The Generated Config&lt;/h2>
&lt;p>The setup script writes a &lt;code>config.yaml&lt;/code> that wires everything together. Here are the pieces that matter:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0:15000&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># admin UI + dashboards (reachable from host)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sqlite:///data/data.db&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># /data is the mounted ./data dir in the container&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/base-costs.json &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-model rates so every request is priced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors: # demo-only&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wildcard CORS. Safe because the port&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># is loopback-bound. Restrict this for real use.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;OPTIONS&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/gpt-4.1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># fallback: cheaper nano model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1-nano&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three things make the dashboard work:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>database.url&lt;/code>&lt;/strong> — the admin UI&amp;rsquo;s Costs/Analytics pages read from this SQLite database. The mock generator writes the same &lt;code>request_logs&lt;/code> schema, so generated traffic and real traffic land in one place.&lt;/li>
&lt;li>&lt;strong>&lt;code>modelCatalog&lt;/code>&lt;/strong> — &lt;code>base-costs.json&lt;/code> holds per-model input/output (and cache) token rates. This is what turns raw token counts into dollars.&lt;/li>
&lt;li>&lt;strong>&lt;code>models&lt;/code>&lt;/strong> — two routes: an explicit &lt;code>openai/gpt-4.1&lt;/code> and a wildcard &lt;code>openai/*&lt;/code> that falls back to the cheaper &lt;code>gpt-4.1-nano&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="send-real-traffic">Send Real Traffic&lt;/h2>
&lt;p>The mock data gets you a populated dashboard, but the gateway is live — send it a real request and watch it get priced alongside the simulated traffic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;openai/gpt-4.1&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from agentgateway!&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Refresh the &lt;strong>Analytics&lt;/strong> page and your request shows up — tokens counted, cost calculated against the model catalog, attributed to the model and provider. Every real call from here on is accounted for the same way.&lt;/p>
&lt;h2 id="manual-installation">Manual Installation&lt;/h2>
&lt;p>Prefer to run the binary directly instead of the Docker demo? You have three options:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Automated installer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Download a platform-specific binary from the GitHub releases page&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># https://github.com/agentgateway/agentgateway/releases&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Run via Docker with your own config mounted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --rm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 127.0.0.1:4000:4000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 127.0.0.1:15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/config.yaml:/config.yaml&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.3.1 --file /config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Point it at a &lt;code>config.yaml&lt;/code> like the one above, and the proxy listens on port &lt;code>4000&lt;/code> with the admin UI on &lt;code>15000&lt;/code>. From there it&amp;rsquo;s the same dashboard — minus the pre-seeded mock data.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done, tear the demo down with the included script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./destroy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This stops and removes the container and the named volume.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>Cost and token visibility is one of those things you don&amp;rsquo;t realize you&amp;rsquo;re missing until a bill lands. agentgateway puts it right in the box: per-model pricing, a built-in dashboard, and grouping by model, provider, and user — no external observability stack required. The Docker demo gets you a populated dashboard in one command so you can see exactly what it looks like before pointing real traffic at it.&lt;/p>
&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">Demo source code&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/">agentgateway documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/">Solo Enterprise for agentgateway&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>You&amp;rsquo;re routing LLM traffic through a gateway. But do you actually know what it &lt;em>costs&lt;/em>? Not the rough monthly invoice from your provider — the real breakdown. Which model burned the most tokens last night? Which user is driving 80% of your spend? Which provider is quietly eating your budget?&lt;/p>
&lt;p>agentgateway answers those questions out of the box. Every request that flows through the proxy is priced against a per-model rate catalog and surfaced in a built-in &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> dashboard. No external observability stack, no Prometheus, no Grafana — just the standalone binary.&lt;/p>
&lt;p>This guide gets you from zero to a fully populated tokenomics dashboard in a single command. We&amp;rsquo;ll use a Docker-based demo that seeds 5,000 simulated requests across 7 days, so the dashboard has something interesting to show you the moment it boots — then we&amp;rsquo;ll send real traffic through it and watch it get priced live.&lt;/p>
&lt;h2 id="why-this-matters">Why This Matters&lt;/h2>
&lt;p>Cost visibility is the FinOps story for AI. As soon as more than one team, agent, or app starts calling LLMs through shared infrastructure, &amp;ldquo;what did this cost and who spent it?&amp;rdquo; becomes a board-level question. agentgateway answers it at the &lt;strong>gateway layer&lt;/strong>, which means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Per-model pricing&lt;/strong> — every request is priced against a rate catalog (input, output, and cache token rates), so dollars show up next to tokens automatically.&lt;/li>
&lt;li>&lt;strong>Group by anything&lt;/strong> — slice spend and tokens by model, provider, user, group, or user agent (Cursor, Claude Code, openai-python, etc.).&lt;/li>
&lt;li>&lt;strong>Zero application changes&lt;/strong> — your apps just point at the gateway. The accounting happens in the proxy, not in your code.&lt;/li>
&lt;li>&lt;strong>One binary&lt;/strong> — the dashboard ships inside agentgateway. There&amp;rsquo;s no separate metrics pipeline to stand up for basic cost visibility.&lt;/li>
&lt;/ul>
&lt;h2 id="what-youll-build">What You&amp;rsquo;ll Build&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl"> ┌──────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────┐ │ agentgateway │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Your apps / │ /v1/chat/ │ ┌────────────────────────┐ │ ┌────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ agents /curl │───completions──────▶│ │ LLM proxy (port 4000) │──┼─────▶│ OpenAI │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────┘ │ └───────────┬────────────┘ │ └────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ priced per │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ model catalog │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌───────────▼────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────┐ localhost:15000 │ │ Admin UI + Dashboard │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Your browser│────────────────────▶│ │ Costs / Analytics │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────┘ │ └───────────┬────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌───────▼───────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ SQLite data.db │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ request_logs │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └───────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway proxies LLM traffic on port &lt;code>4000&lt;/code> and serves its admin UI and dashboards on port &lt;code>15000&lt;/code>. Every request is written to a SQLite database (&lt;code>data.db&lt;/code>) and priced using a model catalog (&lt;code>base-costs.json&lt;/code>). The mock generator writes to the same &lt;code>request_logs&lt;/code> schema, which is why the dashboard is populated before you send a single real request.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-started/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;code>curl&lt;/code>&lt;/li>
&lt;li>An OpenAI API key&lt;/li>
&lt;li>Python 3.11+ (or &lt;a href="https://docs.astral.sh/uv/">&lt;code>uv&lt;/code>&lt;/a>) — used by the mock-data generator&lt;/li>
&lt;/ul>
&lt;h2 id="quick-start-one-command">Quick Start: One Command&lt;/h2>
&lt;p>Clone the demo and run the setup script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/00-standalone-latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;sk-...&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Open &lt;strong>&lt;code>http://localhost:15000/ui/&lt;/code>&lt;/strong> and head to the &lt;strong>Costs&lt;/strong> and &lt;strong>Analytics&lt;/strong> sections — they&amp;rsquo;re already full of data.&lt;/p>
&lt;h3 id="what-the-script-does">What the script does&lt;/h3>
&lt;p>&lt;code>setup.sh&lt;/code> is a single-command bootstrap. Under the hood it:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Runs preflight checks&lt;/strong> — verifies Docker is installed and the daemon is running, &lt;code>curl&lt;/code> is available, &lt;code>OPENAI_API_KEY&lt;/code> is set, and that &lt;code>uv&lt;/code> or Python 3.11+ is present.&lt;/li>
&lt;li>&lt;strong>Downloads the mock-data generator&lt;/strong> (&lt;code>gen-mock-logs.py&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Generates a SQLite database&lt;/strong> with simulated fleet traffic:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gen-mock-logs.py --replace --requests &lt;span class="m">5000&lt;/span> --days &lt;span class="m">7&lt;/span> -o data/data.db
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>Writes &lt;code>config.yaml&lt;/code>&lt;/strong> pointing the gateway at the database and the model rate catalog.&lt;/li>
&lt;li>&lt;strong>Pulls the agentgateway image&lt;/strong> (&lt;code>cr.agentgateway.dev/agentgateway:v1.3.1&lt;/code>) and removes any previous demo container.&lt;/li>
&lt;li>&lt;strong>Seeds a named Docker volume&lt;/strong> (&lt;code>agw-cost-demo-data&lt;/code>) with the generated database.&lt;/li>
&lt;li>&lt;strong>Launches the container&lt;/strong>, binding ports to loopback only:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">127.0.0.1:4000:4000 # LLM proxy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">127.0.0.1:15000:15000 # admin UI + dashboards
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>&lt;strong>Why loopback only?&lt;/strong> The proxy port carries your API credentials. Binding to &lt;code>127.0.0.1&lt;/code> keeps the demo off your network. Don&amp;rsquo;t expose these ports without locking down auth and CORS first.&lt;/p>
&lt;/blockquote>
&lt;h3 id="tuning-the-dataset">Tuning the dataset&lt;/h3>
&lt;p>Want a bigger or smaller demo dataset? Override the &lt;code>REQUESTS&lt;/code> and &lt;code>DAYS&lt;/code> environment variables before running setup:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REQUESTS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">20000&lt;/span> &lt;span class="nv">DAYS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">30&lt;/span> ./setup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="exploring-the-dashboard">Exploring the Dashboard&lt;/h2>
&lt;p>Open &lt;code>http://localhost:15000/ui/&lt;/code> and click &lt;strong>Analytics&lt;/strong>. By default it shows total traffic over the last 24 hours — token volume per hour with a running tally of cost, tokens, and calls.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-total.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-total.png" alt="agentgateway Analytics dashboard showing total token traffic over 24 hours, with cost, tokens, and call counts" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The real power is in &lt;strong>Group by&lt;/strong>. Switch it to &lt;strong>Provider&lt;/strong> and the same traffic splits out by backend — here OpenAI dominates with ~13.3M tokens, followed by Anthropic, Google, and Bedrock. The breakdown table underneath ranks every provider by token consumption.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-provider.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-provider.png" alt="agentgateway Analytics grouped by provider, showing OpenAI, Anthropic, Google, and Bedrock token usage" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Switch &lt;strong>Group by&lt;/strong> to &lt;strong>User&lt;/strong> and you get per-person accounting — exactly the view you need when you&amp;rsquo;re trying to figure out who&amp;rsquo;s driving spend. Each bar in the time series is stacked by user, and the breakdown ranks them by tokens consumed.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-user.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-24-agentgateway-cost-tokenomics-dashboard/analytics-by-user.png" alt="agentgateway Analytics grouped by user, showing per-person token consumption" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>You can group by &lt;strong>Model&lt;/strong>, &lt;strong>Provider&lt;/strong>, &lt;strong>User&lt;/strong>, &lt;strong>Group&lt;/strong>, or &lt;strong>User agent&lt;/strong> (Cursor, Claude Code, openai-python, codex, bifrost, and more), and switch the &lt;strong>Measure&lt;/strong> between tokens and cost. The &lt;strong>Costs&lt;/strong> page focuses the same data on dollars, and &lt;strong>Export&lt;/strong> lets you pull the underlying numbers out for reporting.&lt;/p>
&lt;h2 id="how-it-works-the-generated-config">How It Works: The Generated Config&lt;/h2>
&lt;p>The setup script writes a &lt;code>config.yaml&lt;/code> that wires everything together. Here are the pieces that matter:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">adminAddr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0:15000&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># admin UI + dashboards (reachable from host)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sqlite:///data/data.db&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># /data is the mounted ./data dir in the container&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelCatalog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/base-costs.json &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># per-model rates so every request is priced&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors: # demo-only&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">wildcard CORS. Safe because the port&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># is loopback-bound. Restrict this for real use.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;OPTIONS&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/gpt-4.1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;openai/*&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># fallback: cheaper nano model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openAI&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1-nano&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;$OPENAI_API_KEY&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Three things make the dashboard work:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>database.url&lt;/code>&lt;/strong> — the admin UI&amp;rsquo;s Costs/Analytics pages read from this SQLite database. The mock generator writes the same &lt;code>request_logs&lt;/code> schema, so generated traffic and real traffic land in one place.&lt;/li>
&lt;li>&lt;strong>&lt;code>modelCatalog&lt;/code>&lt;/strong> — &lt;code>base-costs.json&lt;/code> holds per-model input/output (and cache) token rates. This is what turns raw token counts into dollars.&lt;/li>
&lt;li>&lt;strong>&lt;code>models&lt;/code>&lt;/strong> — two routes: an explicit &lt;code>openai/gpt-4.1&lt;/code> and a wildcard &lt;code>openai/*&lt;/code> that falls back to the cheaper &lt;code>gpt-4.1-nano&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h2 id="send-real-traffic">Send Real Traffic&lt;/h2>
&lt;p>The mock data gets you a populated dashboard, but the gateway is live — send it a real request and watch it get priced alongside the simulated traffic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;openai/gpt-4.1&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from agentgateway!&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Refresh the &lt;strong>Analytics&lt;/strong> page and your request shows up — tokens counted, cost calculated against the model catalog, attributed to the model and provider. Every real call from here on is accounted for the same way.&lt;/p>
&lt;h2 id="manual-installation">Manual Installation&lt;/h2>
&lt;p>Prefer to run the binary directly instead of the Docker demo? You have three options:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Automated installer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -sL https://agentgateway.dev/install &lt;span class="p">|&lt;/span> bash
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Download a platform-specific binary from the GitHub releases page&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># https://github.com/agentgateway/agentgateway/releases&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Run via Docker with your own config mounted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run --rm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 127.0.0.1:4000:4000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 127.0.0.1:15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/config.yaml:/config.yaml&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.3.1 --file /config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Point it at a &lt;code>config.yaml&lt;/code> like the one above, and the proxy listens on port &lt;code>4000&lt;/code> with the admin UI on &lt;code>15000&lt;/code>. From there it&amp;rsquo;s the same dashboard — minus the pre-seeded mock data.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done, tear the demo down with the included script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./destroy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This stops and removes the container and the named volume.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>Cost and token visibility is one of those things you don&amp;rsquo;t realize you&amp;rsquo;re missing until a bill lands. agentgateway puts it right in the box: per-model pricing, a built-in dashboard, and grouping by model, provider, and user — no external observability stack required. The Docker demo gets you a populated dashboard in one command so you can see exactly what it looks like before pointing real traffic at it.&lt;/p>
&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/00-standalone-latest">Demo source code&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/">agentgateway documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/">Solo Enterprise for agentgateway&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Do AgentGateway Tool Modes and Headroom Stack? Measuring Two Token-Saving Layers Together</title><link>https://maniak.io/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/</guid><description>&lt;p>In a &lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">previous post&lt;/a> I showed that &lt;a href="https://agentgateway.dev">Enterprise agentgateway&lt;/a> can cut your LLM bill by changing &lt;strong>how&lt;/strong> GitHub&amp;rsquo;s MCP tools are presented to the model — its &lt;code>toolMode&lt;/code> (Standard / Search / Code) shrinks the &lt;strong>tool-catalog tax&lt;/strong>, the 4,781 tokens of tool schema re-injected on every turn.&lt;/p>
&lt;p>But that&amp;rsquo;s only one line item on the bill. &lt;a href="https://github.com/headroomlabs-ai/headroom">&lt;strong>Headroom&lt;/strong>&lt;/a> attacks a &lt;em>different&lt;/em> one: it&amp;rsquo;s a compression proxy that shrinks the &lt;strong>content payload&lt;/strong> — the verbose GitHub JSON results, file contents, and conversation history — before they ever reach the model. It claims 60–95% token savings, including &amp;ldquo;GitHub issue triage 73%.&amp;rdquo;&lt;/p>
&lt;p>So the obvious question: &lt;strong>they touch different layers, so do the savings stack?&lt;/strong> If AgentGateway removes the catalog tax and Headroom removes the payload tax, do you get &lt;em>both&lt;/em> by running them together — or does one make the other redundant?&lt;/p>
&lt;p>I measured it. All the code, manifests, harness, judge, dashboard, and raw results are in the demo repo:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/105-ent-headroom-comp-tokenomics">sebbycorp/agentgateway-demos / 105-ent-headroom-comp-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;strong>TL;DR:&lt;/strong> Yes — but only when there&amp;rsquo;s real payload to compress, and not for free. On a large repo, Headroom stacks on top of every AGW mode (Code &lt;strong>−63%&lt;/strong>, Search &lt;strong>−36%&lt;/strong>). On a small, catalog-dominated repo it barely helps and actually makes Standard mode &lt;strong>49% more expensive&lt;/strong> by busting the provider&amp;rsquo;s prompt cache. And it trades a little accuracy on exact-identifier tasks.&lt;/p>
&lt;h2 id="two-knobs-two-layers">Two knobs, two layers&lt;/h2>
&lt;p>This is the whole mental model, so it&amp;rsquo;s worth being precise:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>What it reduces&lt;/th>
&lt;th>How&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>AGW tool modes&lt;/strong>&lt;/td>
&lt;td>the tool-&lt;strong>catalog&lt;/strong> tax + orchestration round-trips&lt;/td>
&lt;td>Search: 28 tools → 2 meta-tools (−91% catalog). Code: N calls → 1 round-trip, summary-only result&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Headroom&lt;/strong>&lt;/td>
&lt;td>the content &lt;strong>payload&lt;/strong> + history&lt;/td>
&lt;td>ML/AST/JSON compressors (SmartCrusher, CodeCompressor, the Kompress model), reversibly&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>AGW shrinks the &lt;em>schemas and round-trips&lt;/em>. Headroom shrinks the &lt;em>result data and history&lt;/em>. Because they act on different parts of the request, they can — in principle — compound. The experiment is whether they actually do.&lt;/p>
&lt;h2 id="the-setup-at-a-glance">The setup at a glance&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Backend&lt;/strong>&lt;/td>
&lt;td>GitHub&amp;rsquo;s external remote MCP server — &lt;code>api.githubcopilot.com/mcp/readonly&lt;/code> (28 read-only tools)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Front door&lt;/strong>&lt;/td>
&lt;td>Enterprise agentgateway (&lt;code>toolMode&lt;/code> Standard/Search/Code; injects the GitHub PAT as a Bearer token)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Compression layer&lt;/strong>&lt;/td>
&lt;td>Headroom proxy (&lt;code>headroom proxy&lt;/code>, OpenAI-compatible), upstream pointed at the AGW &lt;code>/openai&lt;/code> route&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Model&lt;/strong>&lt;/td>
&lt;td>&lt;code>gpt-5.5&lt;/code> via the agentgateway &lt;code>/openai&lt;/code> route&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scope&lt;/strong>&lt;/td>
&lt;td>A small repo (&lt;code>sebbycorp/agw-tokenomics-sandbox&lt;/code>) and a larger one (&lt;code>sebbycorp/k8s-iceman&lt;/code>), both read-only-pinned&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Matrix&lt;/strong>&lt;/td>
&lt;td>3 tool modes × Headroom OFF/ON × 2 repos = &lt;strong>12 cells&lt;/strong>, 5 questions each + an LLM-judge quality score&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="architecture--two-independent-switches-in-one-pipeline">Architecture — two independent switches in one pipeline&lt;/h2>
&lt;p>The key insight that makes the test clean: the two knobs sit on &lt;strong>different arrows&lt;/strong>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌─&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">OFF&lt;/span> &lt;span class="err">─►&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">openai&lt;/span> &lt;span class="err">──►&lt;/span> &lt;span class="n">OpenAI&lt;/span> &lt;span class="n">gpt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">5.5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">harness&lt;/span> &lt;span class="err">──&lt;/span>&lt;span class="n">build&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="err">──&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="err">┤&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">catalog&lt;/span> &lt;span class="n">baked&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="err">└─&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">ON&lt;/span> &lt;span class="err">──►&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">proxy&lt;/span> &lt;span class="err">─►&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">openai&lt;/span> &lt;span class="err">─►&lt;/span> &lt;span class="n">OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">per&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="k">tool&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">compresses&lt;/span> &lt;span class="n">payload&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">history&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mcp&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">gh&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">std&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="err">──&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">toolMode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">──&lt;/span>&lt;span class="n">TLS&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">PAT&lt;/span>&lt;span class="err">──►&lt;/span> &lt;span class="n">GitHub&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">MCP&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>Knob 1 — AGW &lt;code>toolMode&lt;/code>&lt;/strong> acts on the MCP catalog (the &lt;code>/mcp/gh-*&lt;/code> arrow).&lt;/li>
&lt;li>&lt;strong>Knob 2 — Headroom&lt;/strong> acts on the LLM request body (the &lt;code>/openai&lt;/code> arrow). OFF = harness posts straight to AGW; ON = it posts to the Headroom proxy, which compresses and forwards to AGW.&lt;/li>
&lt;/ul>
&lt;p>Crucially, the AGW catalog effect is &lt;em>independent&lt;/em> of which LLM URL is used: the harness bakes the tool catalog into the request from the MCP &lt;code>tools/list&lt;/code> response, so the two knobs stay orthogonal. Headroom forwards to AGW (not OpenAI directly) so AGW still supplies the model + key and traces every call.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>A gotcha worth flagging.&lt;/strong> Headroom is OpenAI-compatible at &lt;code>/v1/chat/completions&lt;/code>, and you point its upstream at AGW with &lt;code>OPENAI_TARGET_API_URL&lt;/code>. But out of the box, its &lt;strong>semantic cache&lt;/strong> and &lt;strong>CCR tool-injection&lt;/strong> broke the Search/Code tool-orchestration flow — every Search/Code request through Headroom failed with an MCP &lt;code>TaskGroup&lt;/code> error until I launched it with &lt;code>--no-cache --no-ccr-inject-tool --no-ccr-marker&lt;/code>. In front of an MCP tool-calling agent, those flags are mandatory. (A quick sanity check that compression is actually happening: the same 29 KB tool-result blob went &lt;strong>10,859 → 9,772 prompt tokens&lt;/strong> through the proxy.)&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-numbers">The numbers&lt;/h2>
&lt;p>Single run, &lt;code>gpt-5.5&lt;/code> list-price, cache-aware. &lt;strong>Δ = the Headroom saving (ON vs OFF): positive means ON is cheaper, negative means ON is &lt;em>more expensive&lt;/em>.&lt;/strong>&lt;/p>
&lt;h3 id="small-repo--agw-tokenomics-sandbox">Small repo — &lt;code>agw-tokenomics-sandbox&lt;/code>&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>AGW mode&lt;/th>
&lt;th style="text-align:right">OFF cost&lt;/th>
&lt;th style="text-align:right">ON cost&lt;/th>
&lt;th style="text-align:right">Headroom Δ&lt;/th>
&lt;th style="text-align:right">OFF quality&lt;/th>
&lt;th style="text-align:right">ON quality&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">$0.0289&lt;/td>
&lt;td style="text-align:right">$0.0432&lt;/td>
&lt;td style="text-align:right">&lt;strong>−49% (worse)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search&lt;/td>
&lt;td style="text-align:right">$0.0149&lt;/td>
&lt;td style="text-align:right">$0.0138&lt;/td>
&lt;td style="text-align:right">+7%&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">4.2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code&lt;/td>
&lt;td style="text-align:right">$0.0315&lt;/td>
&lt;td style="text-align:right">$0.0204&lt;/td>
&lt;td style="text-align:right">+35%&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">4.8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="large-repo--k8s-iceman">Large repo — &lt;code>k8s-iceman&lt;/code>&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>AGW mode&lt;/th>
&lt;th style="text-align:right">OFF cost&lt;/th>
&lt;th style="text-align:right">ON cost&lt;/th>
&lt;th style="text-align:right">Headroom Δ&lt;/th>
&lt;th style="text-align:right">OFF quality&lt;/th>
&lt;th style="text-align:right">ON quality&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">$0.0424&lt;/td>
&lt;td style="text-align:right">$0.0391&lt;/td>
&lt;td style="text-align:right">+8%&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">4.2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search&lt;/td>
&lt;td style="text-align:right">$0.0269&lt;/td>
&lt;td style="text-align:right">$0.0173&lt;/td>
&lt;td style="text-align:right">&lt;strong>+36%&lt;/strong>&lt;/td>
&lt;td style="text-align:right">4.8&lt;/td>
&lt;td style="text-align:right">4.4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code&lt;/td>
&lt;td style="text-align:right">$0.0623&lt;/td>
&lt;td style="text-align:right">$0.0233&lt;/td>
&lt;td style="text-align:right">&lt;strong>+63%&lt;/strong>&lt;/td>
&lt;td style="text-align:right">3.4&lt;/td>
&lt;td style="text-align:right">4.6&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The live Grafana dashboard (metrics pushed via Prometheus pushgateway) shows the blended cross-repo picture — Search drops from &lt;strong>$0.0209 → $0.0155, a 26% stacked saving&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-overview.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-overview.png" alt="Grafana dashboard: headline stat panels show avg cost/task for Search with Headroom OFF ($0.0209) vs ON ($0.0155) and a 26% stacked saving. Bar panels below show avg cost/task and avg total tokens per task for each AGW mode (code, search, standard) with Headroom OFF vs ON — every ON bar is shorter than its OFF counterpart except Standard." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And the per-repo + quality detail — note Search&amp;rsquo;s first-call tool context is just 429 tokens (the AGW catalog win, independent of Headroom), and the answer-quality panel that keeps the comparison honest:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-per-repo-quality.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-per-repo-quality.png" alt="Grafana dashboard detail: avg cost/task split by repo and Headroom state; first-call tool tokens by AGW mode (code 3K, search 429, standard 5K); and answer quality 0–5 by mode and Headroom state, where ON cells mostly hold 4.2–4.7 against the Standard/OFF baseline of 5.0." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="why-it-comes-out-this-way">Why it comes out this way&lt;/h2>
&lt;p>&lt;strong>1. On a large repo, the layers stack.&lt;/strong> Big result payloads give Headroom something to compress, and AGW has already collapsed the catalog. The biggest stack is &lt;strong>Code + Headroom (−63%)&lt;/strong> — Code returns summaries only, and Headroom squeezes those further. The cheapest &lt;em>reliable&lt;/em> cell overall is &lt;strong>Search + Headroom ($0.0173)&lt;/strong>: AGW removes the −91% catalog tax, Headroom removes the result-JSON tax.&lt;/p>
&lt;p>&lt;strong>2. On a small repo, Headroom can backfire.&lt;/strong> With little result data to compress, its upside is small — and on Standard mode it &lt;em>loses 49%&lt;/em>. The reason is subtle but important: Headroom rewrites the prompt on every turn, which &lt;strong>busts gpt-5.5&amp;rsquo;s prefix cache&lt;/strong>. On a small workload the big, stable 28-tool catalog is exactly what caches well, so the cache you destroy is worth more than the payload you compress. &lt;strong>Payload compression needs payloads.&lt;/strong>&lt;/p>
&lt;p>&lt;strong>3. Cheaper isn&amp;rsquo;t free.&lt;/strong> The judge flagged it: the &amp;ldquo;list recent commits&amp;rdquo; question repeatedly scored &lt;strong>2/5&lt;/strong> under Headroom, because compression mangles high-entropy &lt;strong>commit SHA hashes&lt;/strong>. For tasks that depend on verbatim identifiers — hashes, tokens, IDs — lossy text compression is risky. Most other answers held at 4–5/5.&lt;/p>
&lt;h2 id="when-to-run-both">When to run both&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Large&lt;/span> &lt;span class="n">result&lt;/span> &lt;span class="n">payloads&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">logs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="n">contents&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">big&lt;/span> &lt;span class="n">JSON&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">YES&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="n">Search&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Code&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">stacks&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="err">−&lt;/span>&lt;span class="mi">36&lt;/span>&lt;span class="o">%&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="err">−&lt;/span>&lt;span class="mi">63&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Small&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">catalog&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">dominated&lt;/span> &lt;span class="n">workload&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="n">alone&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">can&lt;/span> &lt;span class="n">hurt&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">prefix&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cache&lt;/span> &lt;span class="n">busting&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Tasks&lt;/span> &lt;span class="n">needing&lt;/span> &lt;span class="n">exact&lt;/span> &lt;span class="n">IDs&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">SHAs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tokens&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">hashes&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">be&lt;/span> &lt;span class="n">careful&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">can&lt;/span> &lt;span class="n">mangle&lt;/span> &lt;span class="n">them&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Any&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">calling&lt;/span> &lt;span class="n">agent&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">needs&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">no&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cache&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">no&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ccr&lt;/span>&lt;span class="o">-*&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="k">break&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>They&amp;rsquo;re &lt;strong>complementary, not competing&lt;/strong> — AGW on the catalog, Headroom on the payload — and on the right workload they genuinely compound.&lt;/p>
&lt;h2 id="reproduce-it-yourself">Reproduce it yourself&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/105-ent-headroom-comp-tokenomics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp .env.example .env &lt;span class="c1"># AGENTGATEWAY_LICENSE_KEY, OPENAI_API_KEY, GITHUB_PAT (read-only), REPO_LARGE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . .env&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh &lt;span class="c1"># kind + AGW + GitHub MCP backends + installs the Headroom proxy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh &lt;span class="c1"># one question, Headroom OFF vs ON&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REPO_LARGE&lt;/span>&lt;span class="o">=&lt;/span>owner/big-readonly-repo ./run_matrix.sh &lt;span class="c1"># full 12-cell matrix + LLM-judge&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A Grafana dashboard (&lt;strong>AGW Tool Modes + Headroom&lt;/strong>) visualizes cost, tokens, and quality OFF vs ON. You can even replay a saved run into Prometheus with &lt;code>observability/replay_to_pushgateway.py&lt;/code> — no extra LLM spend:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward svc/grafana -n observability 3001:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Clean up with &lt;code>./cleanup.sh&lt;/code>.&lt;/p>
&lt;h2 id="takeaways">Takeaways&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>AGW tool modes and Headroom stack on large payloads&lt;/strong> — large repo: Code &lt;strong>−63%&lt;/strong>, Search &lt;strong>−36%&lt;/strong>. Different layers, real compounding.&lt;/li>
&lt;li>&lt;strong>Headroom is payload-dependent.&lt;/strong> No payload, no benefit — and it can &lt;em>regress&lt;/em> cache-friendly workloads by busting the prompt cache (Standard, small repo: &lt;strong>+49% cost&lt;/strong>).&lt;/li>
&lt;li>&lt;strong>Mind the accuracy cost.&lt;/strong> Compression mangles exact identifiers like commit SHAs; gate any compression layer with an answer-quality check.&lt;/li>
&lt;li>&lt;strong>Headroom is not a clean drop-in for MCP.&lt;/strong> Its semantic cache + CCR tool-injection break tool-calling; disable them with &lt;code>--no-cache --no-ccr-inject-tool --no-ccr-marker&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Measure for your own workload.&lt;/strong> This is a single run on two repos — the verdict flips with payload size, just as the &lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">104 tool-modes study&lt;/a> flipped with catalog size.&lt;/li>
&lt;/ol>
&lt;p>The full repo, manifests, harness, judge, and dashboard:
👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/105-ent-headroom-comp-tokenomics">github.com/sebbycorp/agentgateway-demos/tree/main/105-ent-headroom-comp-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;p>&lt;em>Related reading:&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">GitHub MCP Token Economics: Why Search Mode Cuts Your LLM Bill by ~60%&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-06-13-agentgateway-kubernetes-langfuse-cost-analysis.md">One-Script agentgateway + Langfuse on Kubernetes for LLM Cost Analysis&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-20-mcp-multiplexing-tool-access-agentgateway.md">MCP Multiplexing &amp;amp; Tool Access with agentgateway&lt;/a>&lt;/li>
&lt;li>Enterprise agentgateway docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest">https://agentgateway.dev/docs/kubernetes/latest&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>In a &lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">previous post&lt;/a> I showed that &lt;a href="https://agentgateway.dev">Enterprise agentgateway&lt;/a> can cut your LLM bill by changing &lt;strong>how&lt;/strong> GitHub&amp;rsquo;s MCP tools are presented to the model — its &lt;code>toolMode&lt;/code> (Standard / Search / Code) shrinks the &lt;strong>tool-catalog tax&lt;/strong>, the 4,781 tokens of tool schema re-injected on every turn.&lt;/p>
&lt;p>But that&amp;rsquo;s only one line item on the bill. &lt;a href="https://github.com/headroomlabs-ai/headroom">&lt;strong>Headroom&lt;/strong>&lt;/a> attacks a &lt;em>different&lt;/em> one: it&amp;rsquo;s a compression proxy that shrinks the &lt;strong>content payload&lt;/strong> — the verbose GitHub JSON results, file contents, and conversation history — before they ever reach the model. It claims 60–95% token savings, including &amp;ldquo;GitHub issue triage 73%.&amp;rdquo;&lt;/p>
&lt;p>So the obvious question: &lt;strong>they touch different layers, so do the savings stack?&lt;/strong> If AgentGateway removes the catalog tax and Headroom removes the payload tax, do you get &lt;em>both&lt;/em> by running them together — or does one make the other redundant?&lt;/p>
&lt;p>I measured it. All the code, manifests, harness, judge, dashboard, and raw results are in the demo repo:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/105-ent-headroom-comp-tokenomics">sebbycorp/agentgateway-demos / 105-ent-headroom-comp-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;p>&lt;strong>TL;DR:&lt;/strong> Yes — but only when there&amp;rsquo;s real payload to compress, and not for free. On a large repo, Headroom stacks on top of every AGW mode (Code &lt;strong>−63%&lt;/strong>, Search &lt;strong>−36%&lt;/strong>). On a small, catalog-dominated repo it barely helps and actually makes Standard mode &lt;strong>49% more expensive&lt;/strong> by busting the provider&amp;rsquo;s prompt cache. And it trades a little accuracy on exact-identifier tasks.&lt;/p>
&lt;h2 id="two-knobs-two-layers">Two knobs, two layers&lt;/h2>
&lt;p>This is the whole mental model, so it&amp;rsquo;s worth being precise:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>What it reduces&lt;/th>
&lt;th>How&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>AGW tool modes&lt;/strong>&lt;/td>
&lt;td>the tool-&lt;strong>catalog&lt;/strong> tax + orchestration round-trips&lt;/td>
&lt;td>Search: 28 tools → 2 meta-tools (−91% catalog). Code: N calls → 1 round-trip, summary-only result&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Headroom&lt;/strong>&lt;/td>
&lt;td>the content &lt;strong>payload&lt;/strong> + history&lt;/td>
&lt;td>ML/AST/JSON compressors (SmartCrusher, CodeCompressor, the Kompress model), reversibly&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>AGW shrinks the &lt;em>schemas and round-trips&lt;/em>. Headroom shrinks the &lt;em>result data and history&lt;/em>. Because they act on different parts of the request, they can — in principle — compound. The experiment is whether they actually do.&lt;/p>
&lt;h2 id="the-setup-at-a-glance">The setup at a glance&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Backend&lt;/strong>&lt;/td>
&lt;td>GitHub&amp;rsquo;s external remote MCP server — &lt;code>api.githubcopilot.com/mcp/readonly&lt;/code> (28 read-only tools)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Front door&lt;/strong>&lt;/td>
&lt;td>Enterprise agentgateway (&lt;code>toolMode&lt;/code> Standard/Search/Code; injects the GitHub PAT as a Bearer token)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Compression layer&lt;/strong>&lt;/td>
&lt;td>Headroom proxy (&lt;code>headroom proxy&lt;/code>, OpenAI-compatible), upstream pointed at the AGW &lt;code>/openai&lt;/code> route&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Model&lt;/strong>&lt;/td>
&lt;td>&lt;code>gpt-5.5&lt;/code> via the agentgateway &lt;code>/openai&lt;/code> route&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scope&lt;/strong>&lt;/td>
&lt;td>A small repo (&lt;code>sebbycorp/agw-tokenomics-sandbox&lt;/code>) and a larger one (&lt;code>sebbycorp/k8s-iceman&lt;/code>), both read-only-pinned&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Matrix&lt;/strong>&lt;/td>
&lt;td>3 tool modes × Headroom OFF/ON × 2 repos = &lt;strong>12 cells&lt;/strong>, 5 questions each + an LLM-judge quality score&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="architecture--two-independent-switches-in-one-pipeline">Architecture — two independent switches in one pipeline&lt;/h2>
&lt;p>The key insight that makes the test clean: the two knobs sit on &lt;strong>different arrows&lt;/strong>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌─&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">OFF&lt;/span> &lt;span class="err">─►&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">openai&lt;/span> &lt;span class="err">──►&lt;/span> &lt;span class="n">OpenAI&lt;/span> &lt;span class="n">gpt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">5.5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">harness&lt;/span> &lt;span class="err">──&lt;/span>&lt;span class="n">build&lt;/span> &lt;span class="n">request&lt;/span>&lt;span class="err">──&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="err">┤&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">catalog&lt;/span> &lt;span class="n">baked&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="err">└─&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">ON&lt;/span> &lt;span class="err">──►&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">proxy&lt;/span> &lt;span class="err">─►&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">openai&lt;/span> &lt;span class="err">─►&lt;/span> &lt;span class="n">OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">per&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="k">tool&lt;/span> &lt;span class="n">mode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">compresses&lt;/span> &lt;span class="n">payload&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">history&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mcp&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">gh&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">std&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">search&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="n">code&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="err">──&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">toolMode&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">──&lt;/span>&lt;span class="n">TLS&lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="n">PAT&lt;/span>&lt;span class="err">──►&lt;/span> &lt;span class="n">GitHub&lt;/span> &lt;span class="n">remote&lt;/span> &lt;span class="n">MCP&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;strong>Knob 1 — AGW &lt;code>toolMode&lt;/code>&lt;/strong> acts on the MCP catalog (the &lt;code>/mcp/gh-*&lt;/code> arrow).&lt;/li>
&lt;li>&lt;strong>Knob 2 — Headroom&lt;/strong> acts on the LLM request body (the &lt;code>/openai&lt;/code> arrow). OFF = harness posts straight to AGW; ON = it posts to the Headroom proxy, which compresses and forwards to AGW.&lt;/li>
&lt;/ul>
&lt;p>Crucially, the AGW catalog effect is &lt;em>independent&lt;/em> of which LLM URL is used: the harness bakes the tool catalog into the request from the MCP &lt;code>tools/list&lt;/code> response, so the two knobs stay orthogonal. Headroom forwards to AGW (not OpenAI directly) so AGW still supplies the model + key and traces every call.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>A gotcha worth flagging.&lt;/strong> Headroom is OpenAI-compatible at &lt;code>/v1/chat/completions&lt;/code>, and you point its upstream at AGW with &lt;code>OPENAI_TARGET_API_URL&lt;/code>. But out of the box, its &lt;strong>semantic cache&lt;/strong> and &lt;strong>CCR tool-injection&lt;/strong> broke the Search/Code tool-orchestration flow — every Search/Code request through Headroom failed with an MCP &lt;code>TaskGroup&lt;/code> error until I launched it with &lt;code>--no-cache --no-ccr-inject-tool --no-ccr-marker&lt;/code>. In front of an MCP tool-calling agent, those flags are mandatory. (A quick sanity check that compression is actually happening: the same 29 KB tool-result blob went &lt;strong>10,859 → 9,772 prompt tokens&lt;/strong> through the proxy.)&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-numbers">The numbers&lt;/h2>
&lt;p>Single run, &lt;code>gpt-5.5&lt;/code> list-price, cache-aware. &lt;strong>Δ = the Headroom saving (ON vs OFF): positive means ON is cheaper, negative means ON is &lt;em>more expensive&lt;/em>.&lt;/strong>&lt;/p>
&lt;h3 id="small-repo--agw-tokenomics-sandbox">Small repo — &lt;code>agw-tokenomics-sandbox&lt;/code>&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>AGW mode&lt;/th>
&lt;th style="text-align:right">OFF cost&lt;/th>
&lt;th style="text-align:right">ON cost&lt;/th>
&lt;th style="text-align:right">Headroom Δ&lt;/th>
&lt;th style="text-align:right">OFF quality&lt;/th>
&lt;th style="text-align:right">ON quality&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">$0.0289&lt;/td>
&lt;td style="text-align:right">$0.0432&lt;/td>
&lt;td style="text-align:right">&lt;strong>−49% (worse)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search&lt;/td>
&lt;td style="text-align:right">$0.0149&lt;/td>
&lt;td style="text-align:right">$0.0138&lt;/td>
&lt;td style="text-align:right">+7%&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">4.2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code&lt;/td>
&lt;td style="text-align:right">$0.0315&lt;/td>
&lt;td style="text-align:right">$0.0204&lt;/td>
&lt;td style="text-align:right">+35%&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">4.8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="large-repo--k8s-iceman">Large repo — &lt;code>k8s-iceman&lt;/code>&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>AGW mode&lt;/th>
&lt;th style="text-align:right">OFF cost&lt;/th>
&lt;th style="text-align:right">ON cost&lt;/th>
&lt;th style="text-align:right">Headroom Δ&lt;/th>
&lt;th style="text-align:right">OFF quality&lt;/th>
&lt;th style="text-align:right">ON quality&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">$0.0424&lt;/td>
&lt;td style="text-align:right">$0.0391&lt;/td>
&lt;td style="text-align:right">+8%&lt;/td>
&lt;td style="text-align:right">5.0&lt;/td>
&lt;td style="text-align:right">4.2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search&lt;/td>
&lt;td style="text-align:right">$0.0269&lt;/td>
&lt;td style="text-align:right">$0.0173&lt;/td>
&lt;td style="text-align:right">&lt;strong>+36%&lt;/strong>&lt;/td>
&lt;td style="text-align:right">4.8&lt;/td>
&lt;td style="text-align:right">4.4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code&lt;/td>
&lt;td style="text-align:right">$0.0623&lt;/td>
&lt;td style="text-align:right">$0.0233&lt;/td>
&lt;td style="text-align:right">&lt;strong>+63%&lt;/strong>&lt;/td>
&lt;td style="text-align:right">3.4&lt;/td>
&lt;td style="text-align:right">4.6&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The live Grafana dashboard (metrics pushed via Prometheus pushgateway) shows the blended cross-repo picture — Search drops from &lt;strong>$0.0209 → $0.0155, a 26% stacked saving&lt;/strong>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-overview.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-overview.png" alt="Grafana dashboard: headline stat panels show avg cost/task for Search with Headroom OFF ($0.0209) vs ON ($0.0155) and a 26% stacked saving. Bar panels below show avg cost/task and avg total tokens per task for each AGW mode (code, search, standard) with Headroom OFF vs ON — every ON bar is shorter than its OFF counterpart except Standard." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>And the per-repo + quality detail — note Search&amp;rsquo;s first-call tool context is just 429 tokens (the AGW catalog win, independent of Headroom), and the answer-quality panel that keeps the comparison honest:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-per-repo-quality.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-22-headroom-agentgateway-mcp-token-stacking/grafana-per-repo-quality.png" alt="Grafana dashboard detail: avg cost/task split by repo and Headroom state; first-call tool tokens by AGW mode (code 3K, search 429, standard 5K); and answer quality 0–5 by mode and Headroom state, where ON cells mostly hold 4.2–4.7 against the Standard/OFF baseline of 5.0." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="why-it-comes-out-this-way">Why it comes out this way&lt;/h2>
&lt;p>&lt;strong>1. On a large repo, the layers stack.&lt;/strong> Big result payloads give Headroom something to compress, and AGW has already collapsed the catalog. The biggest stack is &lt;strong>Code + Headroom (−63%)&lt;/strong> — Code returns summaries only, and Headroom squeezes those further. The cheapest &lt;em>reliable&lt;/em> cell overall is &lt;strong>Search + Headroom ($0.0173)&lt;/strong>: AGW removes the −91% catalog tax, Headroom removes the result-JSON tax.&lt;/p>
&lt;p>&lt;strong>2. On a small repo, Headroom can backfire.&lt;/strong> With little result data to compress, its upside is small — and on Standard mode it &lt;em>loses 49%&lt;/em>. The reason is subtle but important: Headroom rewrites the prompt on every turn, which &lt;strong>busts gpt-5.5&amp;rsquo;s prefix cache&lt;/strong>. On a small workload the big, stable 28-tool catalog is exactly what caches well, so the cache you destroy is worth more than the payload you compress. &lt;strong>Payload compression needs payloads.&lt;/strong>&lt;/p>
&lt;p>&lt;strong>3. Cheaper isn&amp;rsquo;t free.&lt;/strong> The judge flagged it: the &amp;ldquo;list recent commits&amp;rdquo; question repeatedly scored &lt;strong>2/5&lt;/strong> under Headroom, because compression mangles high-entropy &lt;strong>commit SHA hashes&lt;/strong>. For tasks that depend on verbatim identifiers — hashes, tokens, IDs — lossy text compression is risky. Most other answers held at 4–5/5.&lt;/p>
&lt;h2 id="when-to-run-both">When to run both&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Large&lt;/span> &lt;span class="n">result&lt;/span> &lt;span class="n">payloads&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">logs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="n">contents&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">big&lt;/span> &lt;span class="n">JSON&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">YES&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="n">Search&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Code&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">stacks&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="err">−&lt;/span>&lt;span class="mi">36&lt;/span>&lt;span class="o">%&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="err">−&lt;/span>&lt;span class="mi">63&lt;/span>&lt;span class="o">%&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Small&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">catalog&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">dominated&lt;/span> &lt;span class="n">workload&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">AGW&lt;/span> &lt;span class="n">alone&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">can&lt;/span> &lt;span class="n">hurt&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">prefix&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cache&lt;/span> &lt;span class="n">busting&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Tasks&lt;/span> &lt;span class="n">needing&lt;/span> &lt;span class="n">exact&lt;/span> &lt;span class="n">IDs&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">SHAs&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tokens&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">hashes&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">be&lt;/span> &lt;span class="n">careful&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">can&lt;/span> &lt;span class="n">mangle&lt;/span> &lt;span class="n">them&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Any&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">calling&lt;/span> &lt;span class="n">agent&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">Headroom&lt;/span> &lt;span class="n">needs&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">no&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cache&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">no&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">ccr&lt;/span>&lt;span class="o">-*&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="k">break&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>They&amp;rsquo;re &lt;strong>complementary, not competing&lt;/strong> — AGW on the catalog, Headroom on the payload — and on the right workload they genuinely compound.&lt;/p>
&lt;h2 id="reproduce-it-yourself">Reproduce it yourself&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/105-ent-headroom-comp-tokenomics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp .env.example .env &lt;span class="c1"># AGENTGATEWAY_LICENSE_KEY, OPENAI_API_KEY, GITHUB_PAT (read-only), REPO_LARGE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . .env&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh &lt;span class="c1"># kind + AGW + GitHub MCP backends + installs the Headroom proxy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh &lt;span class="c1"># one question, Headroom OFF vs ON&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">REPO_LARGE&lt;/span>&lt;span class="o">=&lt;/span>owner/big-readonly-repo ./run_matrix.sh &lt;span class="c1"># full 12-cell matrix + LLM-judge&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A Grafana dashboard (&lt;strong>AGW Tool Modes + Headroom&lt;/strong>) visualizes cost, tokens, and quality OFF vs ON. You can even replay a saved run into Prometheus with &lt;code>observability/replay_to_pushgateway.py&lt;/code> — no extra LLM spend:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward svc/grafana -n observability 3001:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Clean up with &lt;code>./cleanup.sh&lt;/code>.&lt;/p>
&lt;h2 id="takeaways">Takeaways&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>AGW tool modes and Headroom stack on large payloads&lt;/strong> — large repo: Code &lt;strong>−63%&lt;/strong>, Search &lt;strong>−36%&lt;/strong>. Different layers, real compounding.&lt;/li>
&lt;li>&lt;strong>Headroom is payload-dependent.&lt;/strong> No payload, no benefit — and it can &lt;em>regress&lt;/em> cache-friendly workloads by busting the prompt cache (Standard, small repo: &lt;strong>+49% cost&lt;/strong>).&lt;/li>
&lt;li>&lt;strong>Mind the accuracy cost.&lt;/strong> Compression mangles exact identifiers like commit SHAs; gate any compression layer with an answer-quality check.&lt;/li>
&lt;li>&lt;strong>Headroom is not a clean drop-in for MCP.&lt;/strong> Its semantic cache + CCR tool-injection break tool-calling; disable them with &lt;code>--no-cache --no-ccr-inject-tool --no-ccr-marker&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Measure for your own workload.&lt;/strong> This is a single run on two repos — the verdict flips with payload size, just as the &lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">104 tool-modes study&lt;/a> flipped with catalog size.&lt;/li>
&lt;/ol>
&lt;p>The full repo, manifests, harness, judge, and dashboard:
👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/105-ent-headroom-comp-tokenomics">github.com/sebbycorp/agentgateway-demos/tree/main/105-ent-headroom-comp-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;p>&lt;em>Related reading:&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="2026-06-20-github-mcp-token-economics-agentgateway-tool-modes.md">GitHub MCP Token Economics: Why Search Mode Cuts Your LLM Bill by ~60%&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-06-13-agentgateway-kubernetes-langfuse-cost-analysis.md">One-Script agentgateway + Langfuse on Kubernetes for LLM Cost Analysis&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-20-mcp-multiplexing-tool-access-agentgateway.md">MCP Multiplexing &amp;amp; Tool Access with agentgateway&lt;/a>&lt;/li>
&lt;li>Enterprise agentgateway docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest">https://agentgateway.dev/docs/kubernetes/latest&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>GitHub MCP Token Economics: Why Search Mode Cuts Your LLM Bill by ~60%</title><link>https://maniak.io/articles/2026-06-20-github-mcp-token-economics-agentgateway-tool-modes/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-20-github-mcp-token-economics-agentgateway-tool-modes/</guid><description>&lt;p>Every time an LLM talks to an MCP server, it has to be &lt;em>told what tools exist&lt;/em>. That tool catalog — the JSON schema of every tool, its parameters, and descriptions — is injected into the prompt on &lt;strong>every single turn&lt;/strong>. For a small server that&amp;rsquo;s a rounding error. For GitHub&amp;rsquo;s MCP server, with 28 read-only tools and verbose schemas, it&amp;rsquo;s &lt;strong>4,781 tokens of pure overhead per call&lt;/strong> — before the model has done any actual work.&lt;/p>
&lt;p>That overhead is the hidden tax of MCP, and it&amp;rsquo;s the subject of this post. Using &lt;a href="https://agentgateway.dev">Enterprise agentgateway&lt;/a> in front of GitHub&amp;rsquo;s official remote MCP server, I measured three different ways to expose those tools to an LLM and tracked the real USD cost of each. The result is clear and a little counter-intuitive: &lt;strong>for a large catalog like GitHub&amp;rsquo;s, Search mode cuts per-call cost by ~60% and wins across conversations too.&lt;/strong>&lt;/p>
&lt;p>All the code, manifests, harness scripts, and raw results are in the demo repo:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/104-ent-github-tokenomics">sebbycorp/agentgateway-demos / 104-ent-github-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;p>This article walks through how it was deployed, what the numbers were, &lt;em>why&lt;/em> they came out this way, and how you can flip your own gateway into Search mode.&lt;/p>
&lt;h2 id="the-setup-at-a-glance">The setup at a glance&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Backend&lt;/strong>&lt;/td>
&lt;td>GitHub&amp;rsquo;s external remote MCP server — &lt;code>api.githubcopilot.com/mcp/readonly&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tools exposed&lt;/strong>&lt;/td>
&lt;td>28 read-only tools (&lt;code>get_*&lt;/code>, &lt;code>list_*&lt;/code>, &lt;code>search_*&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Front door&lt;/strong>&lt;/td>
&lt;td>Enterprise agentgateway (injects the GitHub PAT as a &lt;code>Bearer&lt;/code> token upstream)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Model&lt;/strong>&lt;/td>
&lt;td>&lt;code>gpt-5.5&lt;/code> via the agentgateway &lt;code>/openai&lt;/code> route&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scope&lt;/strong>&lt;/td>
&lt;td>One dedicated public sandbox repo: &lt;a href="https://github.com/sebbycorp/agw-tokenomics-sandbox">&lt;code>sebbycorp/agw-tokenomics-sandbox&lt;/code>&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Pricing (est.)&lt;/strong>&lt;/td>
&lt;td>input &lt;code>$0.005&lt;/code>/1K · cached input &lt;code>$0.0025&lt;/code>/1K · output &lt;code>$0.015&lt;/code>/1K&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Method&lt;/strong>&lt;/td>
&lt;td>5 questions × 3 modes (single-call) + a 5-turn conversation × 3 modes, run &lt;strong>three times&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A key safety property: every question is pinned to a single repo, the gateway only talks to the &lt;strong>&lt;code>/mcp/readonly&lt;/code>&lt;/strong> endpoint (no mutation tools), and the GitHub PAT is fine-grained and scoped to that one repo. The test physically &lt;em>cannot&lt;/em> touch anything else.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>agentgateway sits between the LLM and GitHub. There is &lt;strong>no MCP pod to build or run&lt;/strong> — GitHub&amp;rsquo;s MCP server is external and remote. The gateway targets it over TLS and injects your PAT. The same gateway also fronts the OpenAI-compatible model route, so one proxy sees both sides of the conversation and can meter tokens.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/architecture.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/architecture.svg" alt="Architecture: the test harness sends /openai and /mcp/gh-* requests to the agentgateway proxy running in a kind cluster. The gateway routes /openai to OpenAI gpt-5.5 and three GitHub MCP backends (Standard, Search, Code) to GitHub&amp;amp;rsquo;s remote MCP server, injecting the PAT as a Bearer token and pinning scope to the read-only sandbox repo. A table shows per-call tool context: Standard 28 tools / 4,781 tokens, Search 2 tools / 429 tokens (−91%), Code 1 tool / 3,021 tokens (−37%)." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="the-three-tool-modes">The three tool modes&lt;/h2>
&lt;p>The whole experiment hinges on &lt;strong>how&lt;/strong> the gateway presents GitHub&amp;rsquo;s 28 tools to the model. Enterprise agentgateway&amp;rsquo;s MCP backend supports three &lt;code>toolMode&lt;/code> values, each a different point on the &amp;ldquo;context vs. round-trips&amp;rdquo; trade-off.&lt;/p>
&lt;h3 id="standard--the-catalog-goes-in-every-turn">Standard — the catalog goes in every turn&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>What the model sees:&lt;/strong> all 28 tools, full schemas.&lt;/li>
&lt;li>&lt;strong>Per-call context:&lt;/strong> ~4,781 tokens.&lt;/li>
&lt;li>&lt;strong>Workflow:&lt;/strong> model picks a tool and calls it directly — no discovery step.&lt;/li>
&lt;li>&lt;strong>Cost:&lt;/strong> you pay to re-send the entire catalog on &lt;em>every&lt;/em> turn.&lt;/li>
&lt;/ul>
&lt;h3 id="search--progressive-disclosure-the-winner-here">Search — progressive disclosure (the winner here)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>What the model sees:&lt;/strong> just &lt;strong>2 meta-tools&lt;/strong>, &lt;code>get_tool&lt;/code> and &lt;code>invoke_tool&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Per-call context:&lt;/strong> ~429 tokens — a &lt;strong>91% reduction&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Workflow:&lt;/strong> the model first &lt;em>discovers&lt;/em> the relevant tool&amp;rsquo;s schema, then invokes it. That&amp;rsquo;s an extra round-trip, but each round-trip is tiny.&lt;/li>
&lt;li>&lt;strong>Cost:&lt;/strong> dramatically less context per turn; you only pull in the schema you actually need.&lt;/li>
&lt;/ul>
&lt;h3 id="code--one-tool-batch-in-javascript">Code — one tool, batch in JavaScript&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>What the model sees:&lt;/strong> a single &lt;code>run_code&lt;/code> tool (with a 10-second sandbox timeout).&lt;/li>
&lt;li>&lt;strong>Per-call context:&lt;/strong> ~3,021 tokens — a 37% reduction.&lt;/li>
&lt;li>&lt;strong>Workflow:&lt;/strong> the model writes JavaScript that calls multiple GitHub tools in one submission; only the &lt;strong>final result&lt;/strong> returns to the transcript, keeping conversation history compact.&lt;/li>
&lt;li>&lt;strong>Cost:&lt;/strong> higher first-call overhead than Search, but it shines when individual results are large and can be batched/summarized server-side.&lt;/li>
&lt;/ul>
&lt;p>These three numbers are &lt;strong>deterministic&lt;/strong> — they&amp;rsquo;re a property of the tool catalog and the mode, identical on every run:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Tools advertised&lt;/th>
&lt;th style="text-align:right">First-call tool tokens&lt;/th>
&lt;th style="text-align:right">vs Standard&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Standard&lt;/strong>&lt;/td>
&lt;td style="text-align:right">28&lt;/td>
&lt;td style="text-align:right">4,781&lt;/td>
&lt;td style="text-align:right">—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Search&lt;/strong>&lt;/td>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">&lt;strong>429&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>−91%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Code&lt;/strong>&lt;/td>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">3,021&lt;/td>
&lt;td style="text-align:right">&lt;strong>−37%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="how-it-was-deployed">How it was deployed&lt;/h2>
&lt;p>The whole stack comes up with one script on a local &lt;code>kind&lt;/code> cluster.&lt;/p>
&lt;p>&lt;strong>Prerequisites:&lt;/strong> &lt;code>kind&lt;/code>, &lt;code>kubectl&lt;/code>, &lt;code>helm&lt;/code>, &lt;code>python3&lt;/code> (≥3.10), and three secrets:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cp .env.example .env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Fill in:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># AGENTGATEWAY_LICENSE_KEY (Enterprise agentgateway)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># OPENAI_API_KEY (gpt-5.5 backend)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># GITHUB_PAT (fine-grained, single-repo, READ-ONLY)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . .env&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>deploy.sh&lt;/code> is idempotent and does the full bring-up:&lt;/p>
&lt;ol>
&lt;li>Creates a &lt;code>kind&lt;/code> Kubernetes cluster&lt;/li>
&lt;li>Installs the Gateway API CRDs&lt;/li>
&lt;li>Deploys the Enterprise agentgateway control plane&lt;/li>
&lt;li>Creates the &lt;code>agentgateway-proxy&lt;/code> Gateway&lt;/li>
&lt;li>Wires the OpenAI (&lt;code>gpt-5.5&lt;/code>) backend + &lt;code>/openai&lt;/code> route&lt;/li>
&lt;li>Configures &lt;strong>three&lt;/strong> GitHub MCP backends — one per tool mode — all pointed at the external MCP server with the PAT injected&lt;/li>
&lt;/ol>
&lt;p>The OpenAI side is a plain &lt;code>AgentgatewayBackend&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-5.5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="how-to-enable-search-mode">How to enable Search mode&lt;/h2>
&lt;p>This is the part most people want. The mode is a &lt;strong>single field&lt;/strong> on the Enterprise MCP backend — &lt;code>spec.entMcp.toolMode&lt;/code>. Here is the exact &lt;code>gh-search&lt;/code> backend from the demo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: gh-search, namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entMcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Search &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># &amp;lt;-- this is the whole trick&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.githubcopilot.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StreamableHTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp/readonly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-pat }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">location&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">header&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: Authorization, prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Bearer &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To get the other two modes, you change exactly one line: &lt;code>toolMode: Standard&lt;/code> or &lt;code>toolMode: Code&lt;/code>. The PAT lives in a Kubernetes &lt;code>Secret&lt;/code> (&lt;code>github-pat&lt;/code>) and the gateway adds &lt;code>Authorization: Bearer &amp;lt;PAT&amp;gt;&lt;/code> on every upstream request, so the model never sees the credential. Each backend gets its own &lt;code>HTTPRoute&lt;/code> (&lt;code>/mcp/gh-std&lt;/code>, &lt;code>/mcp/gh-search&lt;/code>, &lt;code>/mcp/gh-code&lt;/code>) so the harness can hit all three side by side.&lt;/p>
&lt;p>Note the path is &lt;code>/mcp/readonly&lt;/code>, &lt;strong>not&lt;/strong> &lt;code>/mcp/all/readonly&lt;/code> — the former exposes only the 28 read tools; the latter would add mutation tools. For a cost benchmark (and for safety) we stay read-only.&lt;/p>
&lt;h2 id="the-5-questions">The 5 questions&lt;/h2>
&lt;p>Every mode answered the same five questions against the sandbox repo:&lt;/p>
&lt;ol>
&lt;li>Describe the repo: description, default branch, primary language.&lt;/li>
&lt;li>List the 5 most recent commits with their messages.&lt;/li>
&lt;li>List the open issues with their titles.&lt;/li>
&lt;li>List the open pull requests with their titles.&lt;/li>
&lt;li>List the files in the &lt;code>src/&lt;/code> directory.&lt;/li>
&lt;/ol>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>Everything below is captured live on the &lt;strong>GitHub — MCP Tool Modes&lt;/strong> Grafana dashboard the demo ships with. Here&amp;rsquo;s the at-a-glance view after a 5-turn conversation per mode:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/grafana-dashboard.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/grafana-dashboard.png" alt="Grafana dashboard from the demo showing conversation cost after 5 turns (Code $0.1594, Search $0.1752, Standard $0.2333), conversation tokens (Code 49K, Search 47K, Standard 72K), cache-read tokens (Code 42K, Search 28K, Standard 55K), a panel explaining that GitHub&amp;amp;rsquo;s large ~4,781-token catalog makes Search cheapest per call and beats Standard in conversation unlike the small-catalog F5 demo, and the list of the five questions asked against the sandbox repo." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The numbers that follow break those panels down in detail.&lt;/p>
&lt;h3 id="single-call--one-question-fresh-session">Single call — one question, fresh session&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Question&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>repo&lt;/td>
&lt;td style="text-align:right">$0.03239&lt;/td>
&lt;td style="text-align:right">$0.01347&lt;/td>
&lt;td style="text-align:right">$0.02330&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>commits&lt;/td>
&lt;td style="text-align:right">$0.03126&lt;/td>
&lt;td style="text-align:right">$0.01726&lt;/td>
&lt;td style="text-align:right">$0.02015&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>issues&lt;/td>
&lt;td style="text-align:right">$0.02707&lt;/td>
&lt;td style="text-align:right">$0.01451&lt;/td>
&lt;td style="text-align:right">$0.01994&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>prs&lt;/td>
&lt;td style="text-align:right">$0.03862&lt;/td>
&lt;td style="text-align:right">$0.00743&lt;/td>
&lt;td style="text-align:right">$0.01870&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>contents&lt;/td>
&lt;td style="text-align:right">$0.03921&lt;/td>
&lt;td style="text-align:right">$0.01246&lt;/td>
&lt;td style="text-align:right">$0.01905&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>average&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.0337&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.0130&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.0202&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Search is cheapest on all five questions — in every one of the three runs&lt;/strong> (~61% under Standard this run, 53–68% across runs). Code beats Standard but can&amp;rsquo;t overtake Search on a small repo: there&amp;rsquo;s too little per-task data for its batching to repay the higher first-call context.&lt;/p>
&lt;h3 id="multi-turn-conversation--cumulative-cost-per-turn-latest-run">Multi-turn conversation — cumulative cost per turn (latest run)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Turn&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">$0.0322&lt;/td>
&lt;td style="text-align:right">$0.0164&lt;/td>
&lt;td style="text-align:right">$0.0221&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">$0.0877&lt;/td>
&lt;td style="text-align:right">$0.0438&lt;/td>
&lt;td style="text-align:right">$0.0489&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">3&lt;/td>
&lt;td style="text-align:right">$0.1316&lt;/td>
&lt;td style="text-align:right">$0.0786&lt;/td>
&lt;td style="text-align:right">$0.0911&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">$0.1758&lt;/td>
&lt;td style="text-align:right">$0.1167&lt;/td>
&lt;td style="text-align:right">$0.1194&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">5&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.2333&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.1752&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.1594&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Totals after 5 turns:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Total tokens&lt;/th>
&lt;th style="text-align:right">Cache-read tokens&lt;/th>
&lt;th style="text-align:right">Cost&lt;/th>
&lt;th style="text-align:right">vs Standard&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Standard&lt;/strong>&lt;/td>
&lt;td style="text-align:right">71,768&lt;/td>
&lt;td style="text-align:right">54,912&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.233&lt;/strong>&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Search&lt;/strong>&lt;/td>
&lt;td style="text-align:right">46,812&lt;/td>
&lt;td style="text-align:right">28,416&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.175&lt;/strong>&lt;/td>
&lt;td style="text-align:right">−25%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Code&lt;/strong>&lt;/td>
&lt;td style="text-align:right">49,020&lt;/td>
&lt;td style="text-align:right">41,856&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.159&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>−32%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Both Search and Code beat Standard. In this particular run Code edged out Search; in the prior two runs Search was cheapest. The constant across &lt;strong>every&lt;/strong> run: &lt;strong>Standard is the most expensive&lt;/strong> — re-sending GitHub&amp;rsquo;s catalog each turn is pricey, so avoiding it always pays.&lt;/p>
&lt;h3 id="cache-analysis-gpt-55-prompt-caching">Cache analysis (gpt-5.5 prompt caching)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Cache-read tokens (5-turn convo)&lt;/th>
&lt;th style="text-align:right">% of total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">54,912&lt;/td>
&lt;td style="text-align:right">77%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search&lt;/td>
&lt;td style="text-align:right">28,416&lt;/td>
&lt;td style="text-align:right">61%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code&lt;/td>
&lt;td style="text-align:right">41,856&lt;/td>
&lt;td style="text-align:right">85%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="does-the-search-win-survive-a-different-cache-discount">Does the Search win survive a different cache discount?&lt;/h3>
&lt;p>A fair question: gpt-5.5&amp;rsquo;s cached input is ~50% off. What if your cache is cheaper?&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Cache discount&lt;/th>
&lt;th style="text-align:right">cached $/1K&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Search ÷ Standard&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>50% off (used here)&lt;/td>
&lt;td style="text-align:right">$0.00250&lt;/td>
&lt;td style="text-align:right">$0.233&lt;/td>
&lt;td style="text-align:right">$0.175&lt;/td>
&lt;td style="text-align:right">&lt;strong>0.75×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>75% off&lt;/td>
&lt;td style="text-align:right">$0.00125&lt;/td>
&lt;td style="text-align:right">$0.165&lt;/td>
&lt;td style="text-align:right">$0.140&lt;/td>
&lt;td style="text-align:right">0.85×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>90% off&lt;/td>
&lt;td style="text-align:right">$0.00050&lt;/td>
&lt;td style="text-align:right">$0.123&lt;/td>
&lt;td style="text-align:right">$0.118&lt;/td>
&lt;td style="text-align:right">0.96×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>100% (cache free)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">$0.00000&lt;/td>
&lt;td style="text-align:right">$0.096&lt;/td>
&lt;td style="text-align:right">$0.104&lt;/td>
&lt;td style="text-align:right">&lt;strong>1.08×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>At realistic cache rates Search clearly wins. The margin narrows as cache gets cheaper, and only at the &lt;em>theoretical&lt;/em> free-cache extreme does Standard&amp;rsquo;s huge-but-cached catalog edge ahead. Net: &lt;strong>Search wins at normal cache rates&lt;/strong>; it&amp;rsquo;s a toss-up only in the free-cache limit. Code, with the largest cached share, is the most cache-rate-robust of the three.&lt;/p>
&lt;h3 id="reproducibility--three-runs">Reproducibility — three runs&lt;/h3>
&lt;p>Single-call average per task:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Run&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">$0.0410&lt;/td>
&lt;td style="text-align:right">$0.0150&lt;/td>
&lt;td style="text-align:right">$0.0335&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">$0.0295&lt;/td>
&lt;td style="text-align:right">$0.0140&lt;/td>
&lt;td style="text-align:right">$0.0200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">3&lt;/td>
&lt;td style="text-align:right">$0.0337&lt;/td>
&lt;td style="text-align:right">$0.0130&lt;/td>
&lt;td style="text-align:right">$0.0202&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Conversation cost after 5 turns:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Run&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">$0.250&lt;/td>
&lt;td style="text-align:right">$0.165&lt;/td>
&lt;td style="text-align:right">$0.256&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">$0.379&lt;/td>
&lt;td style="text-align:right">$0.171&lt;/td>
&lt;td style="text-align:right">$0.238&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">3&lt;/td>
&lt;td style="text-align:right">$0.233&lt;/td>
&lt;td style="text-align:right">$0.175&lt;/td>
&lt;td style="text-align:right">$0.159&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;ul>
&lt;li>&lt;strong>Deterministic:&lt;/strong> first-call context (4,781 / 429 / 3,021) and tools advertised (28 / 2 / 1).&lt;/li>
&lt;li>&lt;strong>Stable:&lt;/strong> Search is cheapest on all 5 single questions, every run; Search &amp;amp; Code both beat Standard in conversation, every run; Search is the most predictable (~$0.17).&lt;/li>
&lt;li>&lt;strong>Noisy:&lt;/strong> Standard&amp;rsquo;s conversation cost ($0.23–$0.38) and the exact Search-vs-Code ordering.&lt;/li>
&lt;/ul>
&lt;p>The headline conclusion is robust to this noise.&lt;/p>
&lt;h2 id="why-the-results-came-out-this-way">Why the results came out this way&lt;/h2>
&lt;p>There are two costs in any MCP conversation, and they pull in opposite directions:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Catalog tax&lt;/strong> — the tool schemas re-sent every turn. Standard pays this in full (4,781 tokens), every single call. Search nearly eliminates it (429 tokens). Code reduces it (3,021 tokens).&lt;/li>
&lt;li>&lt;strong>Round-trip / transcript cost&lt;/strong> — Search&amp;rsquo;s discover-then-invoke pattern adds an extra model call, and a growing conversation re-sends history each turn.&lt;/li>
&lt;/ol>
&lt;p>For GitHub, &lt;strong>the catalog tax dominates&lt;/strong>. The catalog is so large and verbose that paying it on every turn (Standard) swamps the small cost of Search&amp;rsquo;s extra discovery round-trips. So Search wins on single calls &lt;em>and&lt;/em> holds up across conversations.&lt;/p>
&lt;p>This is exactly why it&amp;rsquo;s worth measuring rather than assuming. In a &lt;strong>previous demo (103, the F5 MCP server)&lt;/strong> the catalog was only ~1,588 tokens — small enough that the re-sent transcript, not the catalog, dominated. There, Standard actually &lt;em>won&lt;/em> in conversation and Search was up to ~4.8× worse. Same three modes, opposite verdict:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>F5 (103), ~1,588-tok catalog&lt;/th>
&lt;th>GitHub (104), ~4,781-tok catalog&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Search, single call&lt;/td>
&lt;td>~−18% vs Standard&lt;/td>
&lt;td>&lt;strong>~−60%&lt;/strong> vs Standard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search, 5-turn convo&lt;/td>
&lt;td>&lt;strong>+380% (≈4.8× worse)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>−25% to −55% (better)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Catalog size is the deciding variable.&lt;/strong> The bigger and chattier your MCP server&amp;rsquo;s tool definitions, the more Search mode saves you.&lt;/p>
&lt;h2 id="why-enabling-search-matters">Why enabling Search matters&lt;/h2>
&lt;p>If you&amp;rsquo;re routing a real coding agent or chatops bot at GitHub&amp;rsquo;s MCP server, Standard mode means you&amp;rsquo;re paying a ~4,800-token surcharge on every turn of every conversation, forever. Flipping &lt;code>toolMode: Search&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Cuts single-call cost ~60%&lt;/strong> and conversation cost 25–55% in these tests.&lt;/li>
&lt;li>&lt;strong>Is the most predictable&lt;/strong> option — a tight ~$0.17 per 5-turn chat across all runs, while Standard swings 60%.&lt;/li>
&lt;li>&lt;strong>Scales with catalog growth&lt;/strong> — as GitHub (or any vendor) adds more tools, the catalog tax grows and Search&amp;rsquo;s advantage &lt;em>widens&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Costs you one line of YAML&lt;/strong> and a tiny bit of extra latency from the discovery round-trip.&lt;/li>
&lt;/ul>
&lt;p>Reach for &lt;strong>Code mode&lt;/strong> instead when individual tool results are large (big file contents, long issue threads): batching multiple calls and returning only summaries server-side keeps the transcript small and can pull ahead of Search.&lt;/p>
&lt;h2 id="reproduce-it-yourself">Reproduce it yourself&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/104-ent-github-tokenomics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp .env.example .env &lt;span class="c1"># AGENTGATEWAY_LICENSE_KEY, OPENAI_API_KEY, GITHUB_PAT (read-only, single-repo)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . .env&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward deployment/agentgateway-proxy -n agentgateway-system 8080:80 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward svc/prometheus-prometheus-pushgateway -n observability 9091:9091 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LLM_NO_TEMPERATURE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> ./harness/.venv/bin/python harness/gh_questions.py &lt;span class="c1"># 5 Qs × 3 modes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LLM_NO_TEMPERATURE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> ./harness/.venv/bin/python harness/gh_conversation.py &lt;span class="c1"># 5-turn chat × 3 modes&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Point at a different repo with &lt;code>GH_REPO=owner/name&lt;/code>, or go interactive:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./harness/.venv/bin/python harness/gh_chat.py search &lt;span class="s2">&amp;#34;list the open issues&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A Grafana dashboard (&lt;strong>GitHub — MCP Tool Modes&lt;/strong>) visualizes the token and cost metrics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward svc/grafana -n observability 3001:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Clean up with &lt;code>./cleanup.sh&lt;/code>.&lt;/p>
&lt;h2 id="takeaways">Takeaways&lt;/h2>
&lt;ol>
&lt;li>For a verbose catalog like GitHub&amp;rsquo;s, the per-call context reduction is huge — &lt;strong>Search −91%, Code −37%&lt;/strong> — and it translates directly into dollars.&lt;/li>
&lt;li>&lt;strong>Avoid Standard for a big catalog.&lt;/strong> It&amp;rsquo;s the most expensive and least predictable mode.&lt;/li>
&lt;li>&lt;strong>Search is the safe pick.&lt;/strong> Code can edge ahead when results are large; their order flips run-to-run on small data.&lt;/li>
&lt;li>Caching helps every mode (~61–88% from cache) but doesn&amp;rsquo;t change the ranking.&lt;/li>
&lt;li>&lt;strong>Catalog size and result size decide the winner.&lt;/strong> The F5 demo had the opposite verdict. Measure for &lt;em>your&lt;/em> catalog.&lt;/li>
&lt;/ol>
&lt;p>The full repo, manifests, and harness:
👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/104-ent-github-tokenomics">github.com/sebbycorp/agentgateway-demos/tree/main/104-ent-github-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;p>&lt;em>Related reading:&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="2026-06-13-agentgateway-kubernetes-langfuse-cost-analysis.md">One-Script agentgateway + Langfuse on Kubernetes for LLM Cost Analysis&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-14-llm-observability-agentgateway-langfuse.md">LLM Observability with agentgateway + Langfuse&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-20-mcp-multiplexing-tool-access-agentgateway.md">MCP Multiplexing &amp;amp; Tool Access with agentgateway&lt;/a>&lt;/li>
&lt;li>Enterprise agentgateway docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest">https://agentgateway.dev/docs/kubernetes/latest&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>Every time an LLM talks to an MCP server, it has to be &lt;em>told what tools exist&lt;/em>. That tool catalog — the JSON schema of every tool, its parameters, and descriptions — is injected into the prompt on &lt;strong>every single turn&lt;/strong>. For a small server that&amp;rsquo;s a rounding error. For GitHub&amp;rsquo;s MCP server, with 28 read-only tools and verbose schemas, it&amp;rsquo;s &lt;strong>4,781 tokens of pure overhead per call&lt;/strong> — before the model has done any actual work.&lt;/p>
&lt;p>That overhead is the hidden tax of MCP, and it&amp;rsquo;s the subject of this post. Using &lt;a href="https://agentgateway.dev">Enterprise agentgateway&lt;/a> in front of GitHub&amp;rsquo;s official remote MCP server, I measured three different ways to expose those tools to an LLM and tracked the real USD cost of each. The result is clear and a little counter-intuitive: &lt;strong>for a large catalog like GitHub&amp;rsquo;s, Search mode cuts per-call cost by ~60% and wins across conversations too.&lt;/strong>&lt;/p>
&lt;p>All the code, manifests, harness scripts, and raw results are in the demo repo:&lt;/p>
&lt;p>👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/104-ent-github-tokenomics">sebbycorp/agentgateway-demos / 104-ent-github-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;p>This article walks through how it was deployed, what the numbers were, &lt;em>why&lt;/em> they came out this way, and how you can flip your own gateway into Search mode.&lt;/p>
&lt;h2 id="the-setup-at-a-glance">The setup at a glance&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Backend&lt;/strong>&lt;/td>
&lt;td>GitHub&amp;rsquo;s external remote MCP server — &lt;code>api.githubcopilot.com/mcp/readonly&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tools exposed&lt;/strong>&lt;/td>
&lt;td>28 read-only tools (&lt;code>get_*&lt;/code>, &lt;code>list_*&lt;/code>, &lt;code>search_*&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Front door&lt;/strong>&lt;/td>
&lt;td>Enterprise agentgateway (injects the GitHub PAT as a &lt;code>Bearer&lt;/code> token upstream)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Model&lt;/strong>&lt;/td>
&lt;td>&lt;code>gpt-5.5&lt;/code> via the agentgateway &lt;code>/openai&lt;/code> route&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Scope&lt;/strong>&lt;/td>
&lt;td>One dedicated public sandbox repo: &lt;a href="https://github.com/sebbycorp/agw-tokenomics-sandbox">&lt;code>sebbycorp/agw-tokenomics-sandbox&lt;/code>&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Pricing (est.)&lt;/strong>&lt;/td>
&lt;td>input &lt;code>$0.005&lt;/code>/1K · cached input &lt;code>$0.0025&lt;/code>/1K · output &lt;code>$0.015&lt;/code>/1K&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Method&lt;/strong>&lt;/td>
&lt;td>5 questions × 3 modes (single-call) + a 5-turn conversation × 3 modes, run &lt;strong>three times&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>A key safety property: every question is pinned to a single repo, the gateway only talks to the &lt;strong>&lt;code>/mcp/readonly&lt;/code>&lt;/strong> endpoint (no mutation tools), and the GitHub PAT is fine-grained and scoped to that one repo. The test physically &lt;em>cannot&lt;/em> touch anything else.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>agentgateway sits between the LLM and GitHub. There is &lt;strong>no MCP pod to build or run&lt;/strong> — GitHub&amp;rsquo;s MCP server is external and remote. The gateway targets it over TLS and injects your PAT. The same gateway also fronts the OpenAI-compatible model route, so one proxy sees both sides of the conversation and can meter tokens.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/architecture.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/architecture.svg" alt="Architecture: the test harness sends /openai and /mcp/gh-* requests to the agentgateway proxy running in a kind cluster. The gateway routes /openai to OpenAI gpt-5.5 and three GitHub MCP backends (Standard, Search, Code) to GitHub&amp;amp;rsquo;s remote MCP server, injecting the PAT as a Bearer token and pinning scope to the read-only sandbox repo. A table shows per-call tool context: Standard 28 tools / 4,781 tokens, Search 2 tools / 429 tokens (−91%), Code 1 tool / 3,021 tokens (−37%)." loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="the-three-tool-modes">The three tool modes&lt;/h2>
&lt;p>The whole experiment hinges on &lt;strong>how&lt;/strong> the gateway presents GitHub&amp;rsquo;s 28 tools to the model. Enterprise agentgateway&amp;rsquo;s MCP backend supports three &lt;code>toolMode&lt;/code> values, each a different point on the &amp;ldquo;context vs. round-trips&amp;rdquo; trade-off.&lt;/p>
&lt;h3 id="standard--the-catalog-goes-in-every-turn">Standard — the catalog goes in every turn&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>What the model sees:&lt;/strong> all 28 tools, full schemas.&lt;/li>
&lt;li>&lt;strong>Per-call context:&lt;/strong> ~4,781 tokens.&lt;/li>
&lt;li>&lt;strong>Workflow:&lt;/strong> model picks a tool and calls it directly — no discovery step.&lt;/li>
&lt;li>&lt;strong>Cost:&lt;/strong> you pay to re-send the entire catalog on &lt;em>every&lt;/em> turn.&lt;/li>
&lt;/ul>
&lt;h3 id="search--progressive-disclosure-the-winner-here">Search — progressive disclosure (the winner here)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>What the model sees:&lt;/strong> just &lt;strong>2 meta-tools&lt;/strong>, &lt;code>get_tool&lt;/code> and &lt;code>invoke_tool&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Per-call context:&lt;/strong> ~429 tokens — a &lt;strong>91% reduction&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Workflow:&lt;/strong> the model first &lt;em>discovers&lt;/em> the relevant tool&amp;rsquo;s schema, then invokes it. That&amp;rsquo;s an extra round-trip, but each round-trip is tiny.&lt;/li>
&lt;li>&lt;strong>Cost:&lt;/strong> dramatically less context per turn; you only pull in the schema you actually need.&lt;/li>
&lt;/ul>
&lt;h3 id="code--one-tool-batch-in-javascript">Code — one tool, batch in JavaScript&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>What the model sees:&lt;/strong> a single &lt;code>run_code&lt;/code> tool (with a 10-second sandbox timeout).&lt;/li>
&lt;li>&lt;strong>Per-call context:&lt;/strong> ~3,021 tokens — a 37% reduction.&lt;/li>
&lt;li>&lt;strong>Workflow:&lt;/strong> the model writes JavaScript that calls multiple GitHub tools in one submission; only the &lt;strong>final result&lt;/strong> returns to the transcript, keeping conversation history compact.&lt;/li>
&lt;li>&lt;strong>Cost:&lt;/strong> higher first-call overhead than Search, but it shines when individual results are large and can be batched/summarized server-side.&lt;/li>
&lt;/ul>
&lt;p>These three numbers are &lt;strong>deterministic&lt;/strong> — they&amp;rsquo;re a property of the tool catalog and the mode, identical on every run:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Tools advertised&lt;/th>
&lt;th style="text-align:right">First-call tool tokens&lt;/th>
&lt;th style="text-align:right">vs Standard&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Standard&lt;/strong>&lt;/td>
&lt;td style="text-align:right">28&lt;/td>
&lt;td style="text-align:right">4,781&lt;/td>
&lt;td style="text-align:right">—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Search&lt;/strong>&lt;/td>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">&lt;strong>429&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>−91%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Code&lt;/strong>&lt;/td>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">3,021&lt;/td>
&lt;td style="text-align:right">&lt;strong>−37%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="how-it-was-deployed">How it was deployed&lt;/h2>
&lt;p>The whole stack comes up with one script on a local &lt;code>kind&lt;/code> cluster.&lt;/p>
&lt;p>&lt;strong>Prerequisites:&lt;/strong> &lt;code>kind&lt;/code>, &lt;code>kubectl&lt;/code>, &lt;code>helm&lt;/code>, &lt;code>python3&lt;/code> (≥3.10), and three secrets:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cp .env.example .env
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Fill in:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># AGENTGATEWAY_LICENSE_KEY (Enterprise agentgateway)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># OPENAI_API_KEY (gpt-5.5 backend)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># GITHUB_PAT (fine-grained, single-repo, READ-ONLY)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . .env&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>deploy.sh&lt;/code> is idempotent and does the full bring-up:&lt;/p>
&lt;ol>
&lt;li>Creates a &lt;code>kind&lt;/code> Kubernetes cluster&lt;/li>
&lt;li>Installs the Gateway API CRDs&lt;/li>
&lt;li>Deploys the Enterprise agentgateway control plane&lt;/li>
&lt;li>Creates the &lt;code>agentgateway-proxy&lt;/code> Gateway&lt;/li>
&lt;li>Wires the OpenAI (&lt;code>gpt-5.5&lt;/code>) backend + &lt;code>/openai&lt;/code> route&lt;/li>
&lt;li>Configures &lt;strong>three&lt;/strong> GitHub MCP backends — one per tool mode — all pointed at the external MCP server with the PAT injected&lt;/li>
&lt;/ol>
&lt;p>The OpenAI side is a plain &lt;code>AgentgatewayBackend&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-5.5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="how-to-enable-search-mode">How to enable Search mode&lt;/h2>
&lt;p>This is the part most people want. The mode is a &lt;strong>single field&lt;/strong> on the Enterprise MCP backend — &lt;code>spec.entMcp.toolMode&lt;/code>. Here is the exact &lt;code>gh-search&lt;/code> backend from the demo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: gh-search, namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">entMcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Search &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># &amp;lt;-- this is the whole trick&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.githubcopilot.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StreamableHTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp/readonly&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-pat }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">location&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">header&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name: Authorization, prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Bearer &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To get the other two modes, you change exactly one line: &lt;code>toolMode: Standard&lt;/code> or &lt;code>toolMode: Code&lt;/code>. The PAT lives in a Kubernetes &lt;code>Secret&lt;/code> (&lt;code>github-pat&lt;/code>) and the gateway adds &lt;code>Authorization: Bearer &amp;lt;PAT&amp;gt;&lt;/code> on every upstream request, so the model never sees the credential. Each backend gets its own &lt;code>HTTPRoute&lt;/code> (&lt;code>/mcp/gh-std&lt;/code>, &lt;code>/mcp/gh-search&lt;/code>, &lt;code>/mcp/gh-code&lt;/code>) so the harness can hit all three side by side.&lt;/p>
&lt;p>Note the path is &lt;code>/mcp/readonly&lt;/code>, &lt;strong>not&lt;/strong> &lt;code>/mcp/all/readonly&lt;/code> — the former exposes only the 28 read tools; the latter would add mutation tools. For a cost benchmark (and for safety) we stay read-only.&lt;/p>
&lt;h2 id="the-5-questions">The 5 questions&lt;/h2>
&lt;p>Every mode answered the same five questions against the sandbox repo:&lt;/p>
&lt;ol>
&lt;li>Describe the repo: description, default branch, primary language.&lt;/li>
&lt;li>List the 5 most recent commits with their messages.&lt;/li>
&lt;li>List the open issues with their titles.&lt;/li>
&lt;li>List the open pull requests with their titles.&lt;/li>
&lt;li>List the files in the &lt;code>src/&lt;/code> directory.&lt;/li>
&lt;/ol>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>Everything below is captured live on the &lt;strong>GitHub — MCP Tool Modes&lt;/strong> Grafana dashboard the demo ships with. Here&amp;rsquo;s the at-a-glance view after a 5-turn conversation per mode:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/grafana-dashboard.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-20-github-mcp-token-economics/grafana-dashboard.png" alt="Grafana dashboard from the demo showing conversation cost after 5 turns (Code $0.1594, Search $0.1752, Standard $0.2333), conversation tokens (Code 49K, Search 47K, Standard 72K), cache-read tokens (Code 42K, Search 28K, Standard 55K), a panel explaining that GitHub&amp;amp;rsquo;s large ~4,781-token catalog makes Search cheapest per call and beats Standard in conversation unlike the small-catalog F5 demo, and the list of the five questions asked against the sandbox repo." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The numbers that follow break those panels down in detail.&lt;/p>
&lt;h3 id="single-call--one-question-fresh-session">Single call — one question, fresh session&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Question&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>repo&lt;/td>
&lt;td style="text-align:right">$0.03239&lt;/td>
&lt;td style="text-align:right">$0.01347&lt;/td>
&lt;td style="text-align:right">$0.02330&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>commits&lt;/td>
&lt;td style="text-align:right">$0.03126&lt;/td>
&lt;td style="text-align:right">$0.01726&lt;/td>
&lt;td style="text-align:right">$0.02015&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>issues&lt;/td>
&lt;td style="text-align:right">$0.02707&lt;/td>
&lt;td style="text-align:right">$0.01451&lt;/td>
&lt;td style="text-align:right">$0.01994&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>prs&lt;/td>
&lt;td style="text-align:right">$0.03862&lt;/td>
&lt;td style="text-align:right">$0.00743&lt;/td>
&lt;td style="text-align:right">$0.01870&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>contents&lt;/td>
&lt;td style="text-align:right">$0.03921&lt;/td>
&lt;td style="text-align:right">$0.01246&lt;/td>
&lt;td style="text-align:right">$0.01905&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>average&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.0337&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.0130&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.0202&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Search is cheapest on all five questions — in every one of the three runs&lt;/strong> (~61% under Standard this run, 53–68% across runs). Code beats Standard but can&amp;rsquo;t overtake Search on a small repo: there&amp;rsquo;s too little per-task data for its batching to repay the higher first-call context.&lt;/p>
&lt;h3 id="multi-turn-conversation--cumulative-cost-per-turn-latest-run">Multi-turn conversation — cumulative cost per turn (latest run)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Turn&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">$0.0322&lt;/td>
&lt;td style="text-align:right">$0.0164&lt;/td>
&lt;td style="text-align:right">$0.0221&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">$0.0877&lt;/td>
&lt;td style="text-align:right">$0.0438&lt;/td>
&lt;td style="text-align:right">$0.0489&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">3&lt;/td>
&lt;td style="text-align:right">$0.1316&lt;/td>
&lt;td style="text-align:right">$0.0786&lt;/td>
&lt;td style="text-align:right">$0.0911&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">$0.1758&lt;/td>
&lt;td style="text-align:right">$0.1167&lt;/td>
&lt;td style="text-align:right">$0.1194&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">5&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.2333&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.1752&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.1594&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Totals after 5 turns:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Total tokens&lt;/th>
&lt;th style="text-align:right">Cache-read tokens&lt;/th>
&lt;th style="text-align:right">Cost&lt;/th>
&lt;th style="text-align:right">vs Standard&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Standard&lt;/strong>&lt;/td>
&lt;td style="text-align:right">71,768&lt;/td>
&lt;td style="text-align:right">54,912&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.233&lt;/strong>&lt;/td>
&lt;td style="text-align:right">baseline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Search&lt;/strong>&lt;/td>
&lt;td style="text-align:right">46,812&lt;/td>
&lt;td style="text-align:right">28,416&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.175&lt;/strong>&lt;/td>
&lt;td style="text-align:right">−25%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Code&lt;/strong>&lt;/td>
&lt;td style="text-align:right">49,020&lt;/td>
&lt;td style="text-align:right">41,856&lt;/td>
&lt;td style="text-align:right">&lt;strong>$0.159&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>−32%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Both Search and Code beat Standard. In this particular run Code edged out Search; in the prior two runs Search was cheapest. The constant across &lt;strong>every&lt;/strong> run: &lt;strong>Standard is the most expensive&lt;/strong> — re-sending GitHub&amp;rsquo;s catalog each turn is pricey, so avoiding it always pays.&lt;/p>
&lt;h3 id="cache-analysis-gpt-55-prompt-caching">Cache analysis (gpt-5.5 prompt caching)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Mode&lt;/th>
&lt;th style="text-align:right">Cache-read tokens (5-turn convo)&lt;/th>
&lt;th style="text-align:right">% of total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Standard&lt;/td>
&lt;td style="text-align:right">54,912&lt;/td>
&lt;td style="text-align:right">77%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search&lt;/td>
&lt;td style="text-align:right">28,416&lt;/td>
&lt;td style="text-align:right">61%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code&lt;/td>
&lt;td style="text-align:right">41,856&lt;/td>
&lt;td style="text-align:right">85%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="does-the-search-win-survive-a-different-cache-discount">Does the Search win survive a different cache discount?&lt;/h3>
&lt;p>A fair question: gpt-5.5&amp;rsquo;s cached input is ~50% off. What if your cache is cheaper?&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Cache discount&lt;/th>
&lt;th style="text-align:right">cached $/1K&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Search ÷ Standard&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>50% off (used here)&lt;/td>
&lt;td style="text-align:right">$0.00250&lt;/td>
&lt;td style="text-align:right">$0.233&lt;/td>
&lt;td style="text-align:right">$0.175&lt;/td>
&lt;td style="text-align:right">&lt;strong>0.75×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>75% off&lt;/td>
&lt;td style="text-align:right">$0.00125&lt;/td>
&lt;td style="text-align:right">$0.165&lt;/td>
&lt;td style="text-align:right">$0.140&lt;/td>
&lt;td style="text-align:right">0.85×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>90% off&lt;/td>
&lt;td style="text-align:right">$0.00050&lt;/td>
&lt;td style="text-align:right">$0.123&lt;/td>
&lt;td style="text-align:right">$0.118&lt;/td>
&lt;td style="text-align:right">0.96×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>100% (cache free)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">$0.00000&lt;/td>
&lt;td style="text-align:right">$0.096&lt;/td>
&lt;td style="text-align:right">$0.104&lt;/td>
&lt;td style="text-align:right">&lt;strong>1.08×&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>At realistic cache rates Search clearly wins. The margin narrows as cache gets cheaper, and only at the &lt;em>theoretical&lt;/em> free-cache extreme does Standard&amp;rsquo;s huge-but-cached catalog edge ahead. Net: &lt;strong>Search wins at normal cache rates&lt;/strong>; it&amp;rsquo;s a toss-up only in the free-cache limit. Code, with the largest cached share, is the most cache-rate-robust of the three.&lt;/p>
&lt;h3 id="reproducibility--three-runs">Reproducibility — three runs&lt;/h3>
&lt;p>Single-call average per task:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Run&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">$0.0410&lt;/td>
&lt;td style="text-align:right">$0.0150&lt;/td>
&lt;td style="text-align:right">$0.0335&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">$0.0295&lt;/td>
&lt;td style="text-align:right">$0.0140&lt;/td>
&lt;td style="text-align:right">$0.0200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">3&lt;/td>
&lt;td style="text-align:right">$0.0337&lt;/td>
&lt;td style="text-align:right">$0.0130&lt;/td>
&lt;td style="text-align:right">$0.0202&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Conversation cost after 5 turns:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Run&lt;/th>
&lt;th style="text-align:right">Standard&lt;/th>
&lt;th style="text-align:right">Search&lt;/th>
&lt;th style="text-align:right">Code&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">$0.250&lt;/td>
&lt;td style="text-align:right">$0.165&lt;/td>
&lt;td style="text-align:right">$0.256&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">2&lt;/td>
&lt;td style="text-align:right">$0.379&lt;/td>
&lt;td style="text-align:right">$0.171&lt;/td>
&lt;td style="text-align:right">$0.238&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">3&lt;/td>
&lt;td style="text-align:right">$0.233&lt;/td>
&lt;td style="text-align:right">$0.175&lt;/td>
&lt;td style="text-align:right">$0.159&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;ul>
&lt;li>&lt;strong>Deterministic:&lt;/strong> first-call context (4,781 / 429 / 3,021) and tools advertised (28 / 2 / 1).&lt;/li>
&lt;li>&lt;strong>Stable:&lt;/strong> Search is cheapest on all 5 single questions, every run; Search &amp;amp; Code both beat Standard in conversation, every run; Search is the most predictable (~$0.17).&lt;/li>
&lt;li>&lt;strong>Noisy:&lt;/strong> Standard&amp;rsquo;s conversation cost ($0.23–$0.38) and the exact Search-vs-Code ordering.&lt;/li>
&lt;/ul>
&lt;p>The headline conclusion is robust to this noise.&lt;/p>
&lt;h2 id="why-the-results-came-out-this-way">Why the results came out this way&lt;/h2>
&lt;p>There are two costs in any MCP conversation, and they pull in opposite directions:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Catalog tax&lt;/strong> — the tool schemas re-sent every turn. Standard pays this in full (4,781 tokens), every single call. Search nearly eliminates it (429 tokens). Code reduces it (3,021 tokens).&lt;/li>
&lt;li>&lt;strong>Round-trip / transcript cost&lt;/strong> — Search&amp;rsquo;s discover-then-invoke pattern adds an extra model call, and a growing conversation re-sends history each turn.&lt;/li>
&lt;/ol>
&lt;p>For GitHub, &lt;strong>the catalog tax dominates&lt;/strong>. The catalog is so large and verbose that paying it on every turn (Standard) swamps the small cost of Search&amp;rsquo;s extra discovery round-trips. So Search wins on single calls &lt;em>and&lt;/em> holds up across conversations.&lt;/p>
&lt;p>This is exactly why it&amp;rsquo;s worth measuring rather than assuming. In a &lt;strong>previous demo (103, the F5 MCP server)&lt;/strong> the catalog was only ~1,588 tokens — small enough that the re-sent transcript, not the catalog, dominated. There, Standard actually &lt;em>won&lt;/em> in conversation and Search was up to ~4.8× worse. Same three modes, opposite verdict:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>F5 (103), ~1,588-tok catalog&lt;/th>
&lt;th>GitHub (104), ~4,781-tok catalog&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Search, single call&lt;/td>
&lt;td>~−18% vs Standard&lt;/td>
&lt;td>&lt;strong>~−60%&lt;/strong> vs Standard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search, 5-turn convo&lt;/td>
&lt;td>&lt;strong>+380% (≈4.8× worse)&lt;/strong>&lt;/td>
&lt;td>&lt;strong>−25% to −55% (better)&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Catalog size is the deciding variable.&lt;/strong> The bigger and chattier your MCP server&amp;rsquo;s tool definitions, the more Search mode saves you.&lt;/p>
&lt;h2 id="why-enabling-search-matters">Why enabling Search matters&lt;/h2>
&lt;p>If you&amp;rsquo;re routing a real coding agent or chatops bot at GitHub&amp;rsquo;s MCP server, Standard mode means you&amp;rsquo;re paying a ~4,800-token surcharge on every turn of every conversation, forever. Flipping &lt;code>toolMode: Search&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Cuts single-call cost ~60%&lt;/strong> and conversation cost 25–55% in these tests.&lt;/li>
&lt;li>&lt;strong>Is the most predictable&lt;/strong> option — a tight ~$0.17 per 5-turn chat across all runs, while Standard swings 60%.&lt;/li>
&lt;li>&lt;strong>Scales with catalog growth&lt;/strong> — as GitHub (or any vendor) adds more tools, the catalog tax grows and Search&amp;rsquo;s advantage &lt;em>widens&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Costs you one line of YAML&lt;/strong> and a tiny bit of extra latency from the discovery round-trip.&lt;/li>
&lt;/ul>
&lt;p>Reach for &lt;strong>Code mode&lt;/strong> instead when individual tool results are large (big file contents, long issue threads): batching multiple calls and returning only summaries server-side keeps the transcript small and can pull ahead of Search.&lt;/p>
&lt;h2 id="reproduce-it-yourself">Reproduce it yourself&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/104-ent-github-tokenomics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cp .env.example .env &lt;span class="c1"># AGENTGATEWAY_LICENSE_KEY, OPENAI_API_KEY, GITHUB_PAT (read-only, single-repo)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> . .env&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward deployment/agentgateway-proxy -n agentgateway-system 8080:80 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward svc/prometheus-prometheus-pushgateway -n observability 9091:9091 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LLM_NO_TEMPERATURE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> ./harness/.venv/bin/python harness/gh_questions.py &lt;span class="c1"># 5 Qs × 3 modes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">LLM_NO_TEMPERATURE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> ./harness/.venv/bin/python harness/gh_conversation.py &lt;span class="c1"># 5-turn chat × 3 modes&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Point at a different repo with &lt;code>GH_REPO=owner/name&lt;/code>, or go interactive:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./harness/.venv/bin/python harness/gh_chat.py search &lt;span class="s2">&amp;#34;list the open issues&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A Grafana dashboard (&lt;strong>GitHub — MCP Tool Modes&lt;/strong>) visualizes the token and cost metrics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward svc/grafana -n observability 3001:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Clean up with &lt;code>./cleanup.sh&lt;/code>.&lt;/p>
&lt;h2 id="takeaways">Takeaways&lt;/h2>
&lt;ol>
&lt;li>For a verbose catalog like GitHub&amp;rsquo;s, the per-call context reduction is huge — &lt;strong>Search −91%, Code −37%&lt;/strong> — and it translates directly into dollars.&lt;/li>
&lt;li>&lt;strong>Avoid Standard for a big catalog.&lt;/strong> It&amp;rsquo;s the most expensive and least predictable mode.&lt;/li>
&lt;li>&lt;strong>Search is the safe pick.&lt;/strong> Code can edge ahead when results are large; their order flips run-to-run on small data.&lt;/li>
&lt;li>Caching helps every mode (~61–88% from cache) but doesn&amp;rsquo;t change the ranking.&lt;/li>
&lt;li>&lt;strong>Catalog size and result size decide the winner.&lt;/strong> The F5 demo had the opposite verdict. Measure for &lt;em>your&lt;/em> catalog.&lt;/li>
&lt;/ol>
&lt;p>The full repo, manifests, and harness:
👉 &lt;strong>&lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/104-ent-github-tokenomics">github.com/sebbycorp/agentgateway-demos/tree/main/104-ent-github-tokenomics&lt;/a>&lt;/strong>&lt;/p>
&lt;hr>
&lt;p>&lt;em>Related reading:&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="2026-06-13-agentgateway-kubernetes-langfuse-cost-analysis.md">One-Script agentgateway + Langfuse on Kubernetes for LLM Cost Analysis&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-14-llm-observability-agentgateway-langfuse.md">LLM Observability with agentgateway + Langfuse&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-20-mcp-multiplexing-tool-access-agentgateway.md">MCP Multiplexing &amp;amp; Tool Access with agentgateway&lt;/a>&lt;/li>
&lt;li>Enterprise agentgateway docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest">https://agentgateway.dev/docs/kubernetes/latest&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Running agentgateway on Proxmox (LXC): the Everything MCP server end-to-end</title><link>https://maniak.io/articles/agentgateway-lxc-everything-mcp/</link><pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/agentgateway-lxc-everything-mcp/</guid><description>&lt;h1 id="running-agentgateway-on-proxmox-lxc-the-everything-mcp-server-end-to-end">Running agentgateway on Proxmox (LXC): the Everything MCP server end-to-end&lt;/h1>
&lt;p>&lt;strong>Date:&lt;/strong> June 2026
&lt;strong>Author:&lt;/strong> Sebastian Maniak
&lt;strong>Tags:&lt;/strong> agentgateway, mcp, proxmox, lxc, docker, llm, homelab&lt;/p>
&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>Most agentgateway tutorials assume you have a Kubernetes cluster lying around. This one
doesn&amp;rsquo;t. If you run a homelab on &lt;strong>Proxmox&lt;/strong>, the cheapest, fastest way to get a real
agentgateway instance running is a single &lt;strong>LXC container&lt;/strong> with Docker inside it — no
cluster, no Helm, no control plane to babysit.&lt;/p>
&lt;p>By the end of this guide you&amp;rsquo;ll have:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>agentgateway v1.3.0&lt;/strong> running in an unprivileged LXC container&lt;/li>
&lt;li>The official &lt;strong>&amp;ldquo;Everything&amp;rdquo; MCP server&lt;/strong> federated behind it&lt;/li>
&lt;li>An &lt;strong>OpenAI-compatible LLM proxy&lt;/strong> on port &lt;code>4000&lt;/code>&lt;/li>
&lt;li>The built-in &lt;strong>admin UI&lt;/strong> (logs, analytics, the connection playground) on port &lt;code>15000&lt;/code>&lt;/li>
&lt;li>Request logging and cost tracking backed by SQLite — with no database errors&lt;/li>
&lt;/ul>
&lt;p>The whole thing fits in 2 vCPUs and 2 GB of RAM.&lt;/p>
&lt;h3 id="what-is-agentgateway-and-where-does-the-code-live">What is agentgateway, and where does the code live?&lt;/h3>
&lt;p>&lt;a href="https://github.com/agentgateway/agentgateway">&lt;strong>agentgateway&lt;/strong>&lt;/a> is an open-source
(Apache 2.0) connectivity data plane built specifically for agentic AI workloads. Rather
than bolting agent traffic onto a traditional API gateway, it&amp;rsquo;s protocol- and
session-aware from the ground up, and it unifies three things behind one binary:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM Gateway&lt;/strong> — a unified OpenAI-compatible API in front of many providers, with budgets and load balancing&lt;/li>
&lt;li>&lt;strong>MCP Gateway&lt;/strong> — tool federation across many MCP servers (stdio, HTTP/SSE, Streamable HTTP) behind one endpoint&lt;/li>
&lt;li>&lt;strong>A2A Gateway&lt;/strong> — secure agent-to-agent communication with capability discovery&lt;/li>
&lt;/ul>
&lt;p>It&amp;rsquo;s written mostly in &lt;strong>Rust&lt;/strong> for performance and memory safety (which matters for the
long-lived, high-concurrency sessions MCP and A2A produce), with a Go control surface and
a TypeScript UI.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>GitHub repo:&lt;/strong> &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;strong>Latest release:&lt;/strong> &lt;a href="https://github.com/agentgateway/agentgateway/releases">v1.3.0&lt;/a> (June 18, 2026)&lt;/li>
&lt;li>&lt;strong>Container image:&lt;/strong> &lt;code>cr.agentgateway.dev/agentgateway:v1.3.0&lt;/code>&lt;/li>
&lt;li>&lt;strong>Docs:&lt;/strong> &lt;a href="https://agentgateway.dev/docs/">https://agentgateway.dev/docs/&lt;/a>&lt;/li>
&lt;li>&lt;strong>License:&lt;/strong> Apache 2.0 · ⭐ ~3.4k stars&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>⭐ If this setup is useful to you, star the repo — it&amp;rsquo;s a young project (created
March 2025) that just crossed the v1.x line, and the maintainers ship fast.&lt;/p>
&lt;/blockquote>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>A single LXC holds everything. Docker runs two containers — the Everything MCP server and
agentgateway — and agentgateway talks to the MCP server over stdio via &lt;code>docker exec&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Proxmox host (vmbr0, 172.16.10.0/24)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── LXC 210 &amp;#34;agentgateway&amp;#34; (172.16.10.164, DHCP)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── Docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── mcp-everything (modelcontextprotocol &amp;#34;everything&amp;#34; server)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── agentgateway v1.3.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── :4000 LLM proxy (OpenAI-compatible)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── :3000 MCP gateway (Everything federated here)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── :15000 admin UI (logs + analytics + playground)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Proxmox VE 8.x&lt;/li>
&lt;li>A bridge (&lt;code>vmbr0&lt;/code>) with DHCP on the &lt;code>172.16.10.x&lt;/code> network&lt;/li>
&lt;li>Docker Hub access (or a local registry mirror)&lt;/li>
&lt;li>An LLM provider API key if you want to exercise the LLM proxy (OpenAI in this example)&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-the-lxc-container">Step 1: Create the LXC container&lt;/h2>
&lt;p>We use an unprivileged Alpine container with &lt;code>nesting&lt;/code> enabled so Docker can run inside it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Download the Alpine template&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pveam download &lt;span class="nb">local&lt;/span> alpine-3.20-default_20240908_amd64.tar.xz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create container (CTID 210)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct create &lt;span class="m">210&lt;/span> /var/lib/vz/template/cache/alpine-3.20-default_20240908_amd64.tar.xz &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --hostname agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --cores &lt;span class="m">2&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --memory &lt;span class="m">2048&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --net0 &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>eth0,bridge&lt;span class="o">=&lt;/span>vmbr0,ip&lt;span class="o">=&lt;/span>dhcp &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --rootfs local-lvm:8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --unprivileged &lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --features &lt;span class="nv">nesting&lt;/span>&lt;span class="o">=&lt;/span>1,keyctl&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --onboot &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct start &lt;span class="m">210&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>--features nesting=1,keyctl=1&lt;/code> flags are the important bit — without &lt;code>nesting&lt;/code>,
Docker won&amp;rsquo;t start inside an unprivileged container, and &lt;code>keyctl&lt;/code> keeps Docker&amp;rsquo;s
keyring happy.&lt;/p>
&lt;p>You should now see the container in the Proxmox UI:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/agentgateway-lxc-proxmox.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/agentgateway-lxc-proxmox.png" alt="Proxmox LXC Container 210 (agentgateway)" loading="lazy">
&lt;/a>
&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Tip:&lt;/strong> find the container&amp;rsquo;s DHCP-assigned IP with
&lt;code>pct exec 210 -- ip -4 addr show eth0&lt;/code>. Everything below assumes it landed on
&lt;code>172.16.10.164&lt;/code> — substitute your own.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-2-install-docker-inside-the-lxc">Step 2: Install Docker inside the LXC&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- apk update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- apk add docker docker-compose
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- rc-update add docker boot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- service docker start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Sanity check&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker info
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-deploy-the-everything-mcp-server">Step 3: Deploy the Everything MCP server&lt;/h2>
&lt;p>The &lt;a href="https://github.com/modelcontextprotocol/servers/tree/main/src/everything">Everything server&lt;/a>
is the reference MCP server from the Model Context Protocol project. It exposes every MCP
primitive — tools, prompts, resources, sampling — which makes it perfect for verifying that
your gateway is wired correctly before you connect anything real.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker run -d &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name mcp-everything &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --restart unless-stopped &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> modelcontextprotocol/servers:latest everything
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We keep this container alive and let agentgateway attach to it over stdio with
&lt;code>docker exec&lt;/code> (configured in the next step). That avoids exposing the MCP server&amp;rsquo;s raw
port on the network — the only thing clients ever talk to is the gateway.&lt;/p>
&lt;h2 id="step-4-create-the-agentgateway-configuration">Step 4: Create the agentgateway configuration&lt;/h2>
&lt;p>Create &lt;code>/config/config.yaml&lt;/code> inside the LXC. This single file wires up the LLM proxy, the
MCP target, and CORS so the admin UI can call both planes from the browser.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sqlite:///config/data/data.db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sk-...&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;http://172.16.10.164:15000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">GET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">virtualModels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">everything&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stdio&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cmd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;exec&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;-i&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;mcp-everything&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;node&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;dist/index.js&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;http://172.16.10.164:15000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">GET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exposeHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Mcp-Session-Id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">frontendPolicies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxBufferSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">33554432&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things worth understanding here:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>database.url&lt;/code>&lt;/strong> points at a SQLite file under &lt;code>/config/data&lt;/code>. This is what powers the
Logs and Analytics tabs in the UI — if it can&amp;rsquo;t be created, those tabs throw errors
(see Troubleshooting).&lt;/li>
&lt;li>&lt;strong>The MCP &lt;code>stdio&lt;/code> target&lt;/strong> literally runs &lt;code>docker exec -i mcp-everything node dist/index.js&lt;/code>.
agentgateway speaks JSON-RPC over that pipe, so the gateway and the MCP container share
the same Docker daemon inside the LXC.&lt;/li>
&lt;li>&lt;strong>&lt;code>exposeHeaders: [Mcp-Session-Id]&lt;/code>&lt;/strong> is required for browser-based MCP clients — MCP
sessions are stateful, and the session ID rides in that header.&lt;/li>
&lt;li>&lt;strong>CORS &lt;code>allowOrigins&lt;/code>&lt;/strong> must match the URL you open the UI from. If you reach the UI by
hostname or a different IP, add that origin here too, or the browser will block the calls.&lt;/li>
&lt;li>&lt;strong>&lt;code>maxBufferSize: 33554432&lt;/code>&lt;/strong> (32 MB) gives MCP responses with large resource payloads
room to breathe.&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>🔐 &lt;strong>Don&amp;rsquo;t hardcode keys in production.&lt;/strong> Replace the inline &lt;code>apiKey: &amp;quot;sk-...&amp;quot;&lt;/code> with an
environment variable reference and pass it via &lt;code>-e OPENAI_API_KEY=...&lt;/code> on the
&lt;code>docker run&lt;/code>. Inline is fine for a throwaway homelab box; it is not fine for anything
shared.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-5-run-agentgateway">Step 5: Run agentgateway&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- mkdir -p /config/data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker run -d &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --restart unless-stopped &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v /config:/config &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 4000:4000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 3000:3000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">ADMIN_ADDR&lt;/span>&lt;span class="o">=&lt;/span>0.0.0.0:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.3.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f /config/config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ADMIN_ADDR=0.0.0.0:15000&lt;/code> binds the admin UI to all interfaces so you can reach it from
your workstation — by default it only listens on loopback, which is invisible from outside
the container.&lt;/p>
&lt;p>Tail the logs to confirm a clean start:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker logs -f agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-verify-everything-works">Step 6: Verify everything works&lt;/h2>
&lt;p>Open the UI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">http://172.16.10.164:15000/ui
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM proxy&lt;/strong> on port 4000&lt;/li>
&lt;li>&lt;strong>MCP server (Everything)&lt;/strong> on port 3000, with its tools listed in the playground&lt;/li>
&lt;li>&lt;strong>Admin UI&lt;/strong> on port 15000&lt;/li>
&lt;li>&lt;strong>Logs and Analytics&lt;/strong> populated (no database errors)&lt;/li>
&lt;/ul>
&lt;p>Quick smoke tests from your workstation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># LLM proxy — OpenAI-compatible, so any OpenAI SDK/curl works&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://172.16.10.164:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;openai/gpt-4.1-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Say hello from Proxmox&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># MCP gateway — list the tools Everything exposes through the gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://172.16.10.164:3000/ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;jsonrpc&amp;#34;:&amp;#34;2.0&amp;#34;,&amp;#34;id&amp;#34;:1,&amp;#34;method&amp;#34;:&amp;#34;tools/list&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here&amp;rsquo;s what the working UI looks like:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/agentgateway-ui-working.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/agentgateway-ui-working.png" alt="Agent Gateway UI - Everything Working" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>Docker won&amp;rsquo;t start in the LXC.&lt;/strong> You almost certainly created the container without
&lt;code>nesting=1&lt;/code>. Add it with
&lt;code>pct set 210 --features nesting=1,keyctl=1&lt;/code> and restart the container.&lt;/p>
&lt;p>&lt;strong>Logs/Analytics tabs show a database error.&lt;/strong> The &lt;code>/config/data&lt;/code> directory didn&amp;rsquo;t exist
when the container started, so SQLite couldn&amp;rsquo;t create the file. Run
&lt;code>pct exec 210 -- mkdir -p /config/data&lt;/code>, then recreate the container (the &lt;code>-v /config:/config&lt;/code>
mount persists it).&lt;/p>
&lt;p>&lt;strong>UI loads but API calls fail with CORS errors.&lt;/strong> The origin you opened the UI from isn&amp;rsquo;t
in &lt;code>allowOrigins&lt;/code>. Match it exactly — scheme, IP/host, and port — for both the &lt;code>llm&lt;/code> and
&lt;code>mcp&lt;/code> CORS policies.&lt;/p>
&lt;p>&lt;strong>MCP target shows no tools.&lt;/strong> Confirm the Everything container is up
(&lt;code>pct exec 210 -- docker ps&lt;/code>) and that the &lt;code>stdio&lt;/code> &lt;code>args&lt;/code> point at the right entrypoint
(&lt;code>node dist/index.js&lt;/code>). &lt;code>docker logs mcp-everything&lt;/code> will tell you if it crashed on start.&lt;/p>
&lt;h2 id="final-notes">Final notes&lt;/h2>
&lt;ul>
&lt;li>The container IP is &lt;code>172.16.10.164&lt;/code> (DHCP) — pin a reservation on your router if you want it stable.&lt;/li>
&lt;li>All important ports are bound to &lt;code>0.0.0.0&lt;/code>; if this box isn&amp;rsquo;t on a trusted LAN, put it behind a firewall or reverse proxy with auth.&lt;/li>
&lt;li>The Everything MCP server is connected via Docker &lt;code>exec&lt;/code> stdio, so it&amp;rsquo;s never exposed directly.&lt;/li>
&lt;/ul>
&lt;p>This gives you a production-shaped agentgateway — LLM proxy, MCP federation, observability,
and a UI — running on a single Proxmox LXC. From here, swap the Everything server for real
MCP servers, add more LLM providers to the &lt;code>models&lt;/code> list, or layer in RBAC and rate limiting
from the &lt;a href="https://agentgateway.dev/docs/">agentgateway docs&lt;/a>.&lt;/p>
&lt;h3 id="go-deeper">Go deeper&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Source &amp;amp; releases:&lt;/strong> &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;strong>Documentation:&lt;/strong> &lt;a href="https://agentgateway.dev/docs/">https://agentgateway.dev/docs/&lt;/a>&lt;/li>
&lt;li>&lt;strong>Everything MCP server:&lt;/strong> &lt;a href="https://github.com/modelcontextprotocol/servers/tree/main/src/everything">https://github.com/modelcontextprotocol/servers/tree/main/src/everything&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h1 id="running-agentgateway-on-proxmox-lxc-the-everything-mcp-server-end-to-end">Running agentgateway on Proxmox (LXC): the Everything MCP server end-to-end&lt;/h1>
&lt;p>&lt;strong>Date:&lt;/strong> June 2026
&lt;strong>Author:&lt;/strong> Sebastian Maniak
&lt;strong>Tags:&lt;/strong> agentgateway, mcp, proxmox, lxc, docker, llm, homelab&lt;/p>
&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>Most agentgateway tutorials assume you have a Kubernetes cluster lying around. This one
doesn&amp;rsquo;t. If you run a homelab on &lt;strong>Proxmox&lt;/strong>, the cheapest, fastest way to get a real
agentgateway instance running is a single &lt;strong>LXC container&lt;/strong> with Docker inside it — no
cluster, no Helm, no control plane to babysit.&lt;/p>
&lt;p>By the end of this guide you&amp;rsquo;ll have:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>agentgateway v1.3.0&lt;/strong> running in an unprivileged LXC container&lt;/li>
&lt;li>The official &lt;strong>&amp;ldquo;Everything&amp;rdquo; MCP server&lt;/strong> federated behind it&lt;/li>
&lt;li>An &lt;strong>OpenAI-compatible LLM proxy&lt;/strong> on port &lt;code>4000&lt;/code>&lt;/li>
&lt;li>The built-in &lt;strong>admin UI&lt;/strong> (logs, analytics, the connection playground) on port &lt;code>15000&lt;/code>&lt;/li>
&lt;li>Request logging and cost tracking backed by SQLite — with no database errors&lt;/li>
&lt;/ul>
&lt;p>The whole thing fits in 2 vCPUs and 2 GB of RAM.&lt;/p>
&lt;h3 id="what-is-agentgateway-and-where-does-the-code-live">What is agentgateway, and where does the code live?&lt;/h3>
&lt;p>&lt;a href="https://github.com/agentgateway/agentgateway">&lt;strong>agentgateway&lt;/strong>&lt;/a> is an open-source
(Apache 2.0) connectivity data plane built specifically for agentic AI workloads. Rather
than bolting agent traffic onto a traditional API gateway, it&amp;rsquo;s protocol- and
session-aware from the ground up, and it unifies three things behind one binary:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM Gateway&lt;/strong> — a unified OpenAI-compatible API in front of many providers, with budgets and load balancing&lt;/li>
&lt;li>&lt;strong>MCP Gateway&lt;/strong> — tool federation across many MCP servers (stdio, HTTP/SSE, Streamable HTTP) behind one endpoint&lt;/li>
&lt;li>&lt;strong>A2A Gateway&lt;/strong> — secure agent-to-agent communication with capability discovery&lt;/li>
&lt;/ul>
&lt;p>It&amp;rsquo;s written mostly in &lt;strong>Rust&lt;/strong> for performance and memory safety (which matters for the
long-lived, high-concurrency sessions MCP and A2A produce), with a Go control surface and
a TypeScript UI.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>GitHub repo:&lt;/strong> &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;strong>Latest release:&lt;/strong> &lt;a href="https://github.com/agentgateway/agentgateway/releases">v1.3.0&lt;/a> (June 18, 2026)&lt;/li>
&lt;li>&lt;strong>Container image:&lt;/strong> &lt;code>cr.agentgateway.dev/agentgateway:v1.3.0&lt;/code>&lt;/li>
&lt;li>&lt;strong>Docs:&lt;/strong> &lt;a href="https://agentgateway.dev/docs/">https://agentgateway.dev/docs/&lt;/a>&lt;/li>
&lt;li>&lt;strong>License:&lt;/strong> Apache 2.0 · ⭐ ~3.4k stars&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>⭐ If this setup is useful to you, star the repo — it&amp;rsquo;s a young project (created
March 2025) that just crossed the v1.x line, and the maintainers ship fast.&lt;/p>
&lt;/blockquote>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>A single LXC holds everything. Docker runs two containers — the Everything MCP server and
agentgateway — and agentgateway talks to the MCP server over stdio via &lt;code>docker exec&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Proxmox host (vmbr0, 172.16.10.0/24)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── LXC 210 &amp;#34;agentgateway&amp;#34; (172.16.10.164, DHCP)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── Docker
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── mcp-everything (modelcontextprotocol &amp;#34;everything&amp;#34; server)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── agentgateway v1.3.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── :4000 LLM proxy (OpenAI-compatible)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── :3000 MCP gateway (Everything federated here)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── :15000 admin UI (logs + analytics + playground)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Proxmox VE 8.x&lt;/li>
&lt;li>A bridge (&lt;code>vmbr0&lt;/code>) with DHCP on the &lt;code>172.16.10.x&lt;/code> network&lt;/li>
&lt;li>Docker Hub access (or a local registry mirror)&lt;/li>
&lt;li>An LLM provider API key if you want to exercise the LLM proxy (OpenAI in this example)&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-the-lxc-container">Step 1: Create the LXC container&lt;/h2>
&lt;p>We use an unprivileged Alpine container with &lt;code>nesting&lt;/code> enabled so Docker can run inside it.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Download the Alpine template&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pveam download &lt;span class="nb">local&lt;/span> alpine-3.20-default_20240908_amd64.tar.xz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create container (CTID 210)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct create &lt;span class="m">210&lt;/span> /var/lib/vz/template/cache/alpine-3.20-default_20240908_amd64.tar.xz &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --hostname agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --cores &lt;span class="m">2&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --memory &lt;span class="m">2048&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --net0 &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>eth0,bridge&lt;span class="o">=&lt;/span>vmbr0,ip&lt;span class="o">=&lt;/span>dhcp &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --rootfs local-lvm:8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --unprivileged &lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --features &lt;span class="nv">nesting&lt;/span>&lt;span class="o">=&lt;/span>1,keyctl&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --onboot &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct start &lt;span class="m">210&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>--features nesting=1,keyctl=1&lt;/code> flags are the important bit — without &lt;code>nesting&lt;/code>,
Docker won&amp;rsquo;t start inside an unprivileged container, and &lt;code>keyctl&lt;/code> keeps Docker&amp;rsquo;s
keyring happy.&lt;/p>
&lt;p>You should now see the container in the Proxmox UI:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/agentgateway-lxc-proxmox.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/agentgateway-lxc-proxmox.png" alt="Proxmox LXC Container 210 (agentgateway)" loading="lazy">
&lt;/a>
&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Tip:&lt;/strong> find the container&amp;rsquo;s DHCP-assigned IP with
&lt;code>pct exec 210 -- ip -4 addr show eth0&lt;/code>. Everything below assumes it landed on
&lt;code>172.16.10.164&lt;/code> — substitute your own.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-2-install-docker-inside-the-lxc">Step 2: Install Docker inside the LXC&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- apk update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- apk add docker docker-compose
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- rc-update add docker boot
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- service docker start
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Sanity check&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker info
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-deploy-the-everything-mcp-server">Step 3: Deploy the Everything MCP server&lt;/h2>
&lt;p>The &lt;a href="https://github.com/modelcontextprotocol/servers/tree/main/src/everything">Everything server&lt;/a>
is the reference MCP server from the Model Context Protocol project. It exposes every MCP
primitive — tools, prompts, resources, sampling — which makes it perfect for verifying that
your gateway is wired correctly before you connect anything real.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker run -d &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name mcp-everything &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --restart unless-stopped &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> modelcontextprotocol/servers:latest everything
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We keep this container alive and let agentgateway attach to it over stdio with
&lt;code>docker exec&lt;/code> (configured in the next step). That avoids exposing the MCP server&amp;rsquo;s raw
port on the network — the only thing clients ever talk to is the gateway.&lt;/p>
&lt;h2 id="step-4-create-the-agentgateway-configuration">Step 4: Create the agentgateway configuration&lt;/h2>
&lt;p>Create &lt;code>/config/config.yaml&lt;/code> inside the LXC. This single file wires up the LLM proxy, the
MCP target, and CORS so the admin UI can call both planes from the browser.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sqlite:///config/data/data.db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">models&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/*&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4.1-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;sk-...&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;http://172.16.10.164:15000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">GET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">virtualModels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">everything&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stdio&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cmd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;exec&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;-i&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;mcp-everything&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;node&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;dist/index.js&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowOrigins&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;http://172.16.10.164:15000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowMethods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">GET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">POST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exposeHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">Mcp-Session-Id&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">frontendPolicies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxBufferSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">33554432&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things worth understanding here:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>database.url&lt;/code>&lt;/strong> points at a SQLite file under &lt;code>/config/data&lt;/code>. This is what powers the
Logs and Analytics tabs in the UI — if it can&amp;rsquo;t be created, those tabs throw errors
(see Troubleshooting).&lt;/li>
&lt;li>&lt;strong>The MCP &lt;code>stdio&lt;/code> target&lt;/strong> literally runs &lt;code>docker exec -i mcp-everything node dist/index.js&lt;/code>.
agentgateway speaks JSON-RPC over that pipe, so the gateway and the MCP container share
the same Docker daemon inside the LXC.&lt;/li>
&lt;li>&lt;strong>&lt;code>exposeHeaders: [Mcp-Session-Id]&lt;/code>&lt;/strong> is required for browser-based MCP clients — MCP
sessions are stateful, and the session ID rides in that header.&lt;/li>
&lt;li>&lt;strong>CORS &lt;code>allowOrigins&lt;/code>&lt;/strong> must match the URL you open the UI from. If you reach the UI by
hostname or a different IP, add that origin here too, or the browser will block the calls.&lt;/li>
&lt;li>&lt;strong>&lt;code>maxBufferSize: 33554432&lt;/code>&lt;/strong> (32 MB) gives MCP responses with large resource payloads
room to breathe.&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>🔐 &lt;strong>Don&amp;rsquo;t hardcode keys in production.&lt;/strong> Replace the inline &lt;code>apiKey: &amp;quot;sk-...&amp;quot;&lt;/code> with an
environment variable reference and pass it via &lt;code>-e OPENAI_API_KEY=...&lt;/code> on the
&lt;code>docker run&lt;/code>. Inline is fine for a throwaway homelab box; it is not fine for anything
shared.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-5-run-agentgateway">Step 5: Run agentgateway&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- mkdir -p /config/data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker run -d &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --name agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --restart unless-stopped &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v /config:/config &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 4000:4000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 3000:3000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -p 15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -e &lt;span class="nv">ADMIN_ADDR&lt;/span>&lt;span class="o">=&lt;/span>0.0.0.0:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.3.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f /config/config.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ADMIN_ADDR=0.0.0.0:15000&lt;/code> binds the admin UI to all interfaces so you can reach it from
your workstation — by default it only listens on loopback, which is invisible from outside
the container.&lt;/p>
&lt;p>Tail the logs to confirm a clean start:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pct &lt;span class="nb">exec&lt;/span> &lt;span class="m">210&lt;/span> -- docker logs -f agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-verify-everything-works">Step 6: Verify everything works&lt;/h2>
&lt;p>Open the UI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">http://172.16.10.164:15000/ui
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM proxy&lt;/strong> on port 4000&lt;/li>
&lt;li>&lt;strong>MCP server (Everything)&lt;/strong> on port 3000, with its tools listed in the playground&lt;/li>
&lt;li>&lt;strong>Admin UI&lt;/strong> on port 15000&lt;/li>
&lt;li>&lt;strong>Logs and Analytics&lt;/strong> populated (no database errors)&lt;/li>
&lt;/ul>
&lt;p>Quick smoke tests from your workstation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># LLM proxy — OpenAI-compatible, so any OpenAI SDK/curl works&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://172.16.10.164:4000/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;openai/gpt-4.1-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Say hello from Proxmox&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># MCP gateway — list the tools Everything exposes through the gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://172.16.10.164:3000/ &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;jsonrpc&amp;#34;:&amp;#34;2.0&amp;#34;,&amp;#34;id&amp;#34;:1,&amp;#34;method&amp;#34;:&amp;#34;tools/list&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here&amp;rsquo;s what the working UI looks like:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/agentgateway-ui-working.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/agentgateway-ui-working.png" alt="Agent Gateway UI - Everything Working" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>Docker won&amp;rsquo;t start in the LXC.&lt;/strong> You almost certainly created the container without
&lt;code>nesting=1&lt;/code>. Add it with
&lt;code>pct set 210 --features nesting=1,keyctl=1&lt;/code> and restart the container.&lt;/p>
&lt;p>&lt;strong>Logs/Analytics tabs show a database error.&lt;/strong> The &lt;code>/config/data&lt;/code> directory didn&amp;rsquo;t exist
when the container started, so SQLite couldn&amp;rsquo;t create the file. Run
&lt;code>pct exec 210 -- mkdir -p /config/data&lt;/code>, then recreate the container (the &lt;code>-v /config:/config&lt;/code>
mount persists it).&lt;/p>
&lt;p>&lt;strong>UI loads but API calls fail with CORS errors.&lt;/strong> The origin you opened the UI from isn&amp;rsquo;t
in &lt;code>allowOrigins&lt;/code>. Match it exactly — scheme, IP/host, and port — for both the &lt;code>llm&lt;/code> and
&lt;code>mcp&lt;/code> CORS policies.&lt;/p>
&lt;p>&lt;strong>MCP target shows no tools.&lt;/strong> Confirm the Everything container is up
(&lt;code>pct exec 210 -- docker ps&lt;/code>) and that the &lt;code>stdio&lt;/code> &lt;code>args&lt;/code> point at the right entrypoint
(&lt;code>node dist/index.js&lt;/code>). &lt;code>docker logs mcp-everything&lt;/code> will tell you if it crashed on start.&lt;/p>
&lt;h2 id="final-notes">Final notes&lt;/h2>
&lt;ul>
&lt;li>The container IP is &lt;code>172.16.10.164&lt;/code> (DHCP) — pin a reservation on your router if you want it stable.&lt;/li>
&lt;li>All important ports are bound to &lt;code>0.0.0.0&lt;/code>; if this box isn&amp;rsquo;t on a trusted LAN, put it behind a firewall or reverse proxy with auth.&lt;/li>
&lt;li>The Everything MCP server is connected via Docker &lt;code>exec&lt;/code> stdio, so it&amp;rsquo;s never exposed directly.&lt;/li>
&lt;/ul>
&lt;p>This gives you a production-shaped agentgateway — LLM proxy, MCP federation, observability,
and a UI — running on a single Proxmox LXC. From here, swap the Everything server for real
MCP servers, add more LLM providers to the &lt;code>models&lt;/code> list, or layer in RBAC and rate limiting
from the &lt;a href="https://agentgateway.dev/docs/">agentgateway docs&lt;/a>.&lt;/p>
&lt;h3 id="go-deeper">Go deeper&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Source &amp;amp; releases:&lt;/strong> &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;strong>Documentation:&lt;/strong> &lt;a href="https://agentgateway.dev/docs/">https://agentgateway.dev/docs/&lt;/a>&lt;/li>
&lt;li>&lt;strong>Everything MCP server:&lt;/strong> &lt;a href="https://github.com/modelcontextprotocol/servers/tree/main/src/everything">https://github.com/modelcontextprotocol/servers/tree/main/src/everything&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>One-Script Deployment: agentgateway + Self-Hosted Langfuse on Kubernetes for LLM Cost Analysis</title><link>https://maniak.io/articles/2026-06-13-agentgateway-kubernetes-langfuse-cost-analysis/</link><pubDate>Sat, 13 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-13-agentgateway-kubernetes-langfuse-cost-analysis/</guid><description>&lt;p>Running production-grade LLM gateways with full observability usually involves many manual steps across Helm charts, CRDs, tracing configuration, and UI key management.&lt;/p>
&lt;p>The &lt;strong>agentgateway-demos/09-k8s-langfuse&lt;/strong> demo eliminates almost all of that friction by making &lt;strong>agentgateway the central cost-control and observability plane&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>agentgateway&lt;/strong> sits in front of your models, inspects every request/response, emits rich GenAI OpenTelemetry traces (model name, input/output tokens, full prompt + completion, latency, user/session attribution), and forwards them directly to Langfuse. Once you set per-model pricing in Langfuse, you get automatic USD cost tracking, spend dashboards, and usage analytics — all without any external SaaS or manual log shipping.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;p>A single &lt;code>./deploy.sh&lt;/code> that provisions:&lt;/p>
&lt;ul>
&lt;li>A dedicated kind cluster (&lt;code>agw-k8s-langfuse&lt;/code>)&lt;/li>
&lt;li>Full self-hosted Langfuse (PostgreSQL + ClickHouse + Redis + web/worker) via the official Helm chart, tuned for kind&lt;/li>
&lt;li>agentgateway CRDs, controller, GatewayClass, and proxy (v1.1.0)&lt;/li>
&lt;li>Pre-configured &lt;code>AgentgatewayBackend&lt;/code> + &lt;code>HTTPRoute&lt;/code> routing &lt;code>/v1/*&lt;/code> to your local OpenAI-compatible model (e.g. Qwen running on your host)&lt;/li>
&lt;li>Direct OTLP/HTTP tracing from agentgateway → Langfuse (no collector sidecar required)&lt;/li>
&lt;/ul>
&lt;p>After one follow-up script with your project keys, &lt;strong>agentgateway&lt;/strong> automatically sends every LLM call to Langfuse with the data needed for real cost management.&lt;/p>
&lt;h2 id="quick-start-the-automated-path">Quick Start (The Automated Path)&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/09-k8s-langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The script handles:&lt;/p>
&lt;ul>
&lt;li>kind cluster creation&lt;/li>
&lt;li>Gateway API CRDs&lt;/li>
&lt;li>Langfuse Helm install + phased readiness waits (databases first)&lt;/li>
&lt;li>agentgateway controller + CRDs&lt;/li>
&lt;li>Base &lt;code>AgentgatewayParameters&lt;/code>&lt;/li>
&lt;li>Gateway + &lt;code>spark&lt;/code> backend + &lt;code>spark-route&lt;/code> HTTPRoute&lt;/li>
&lt;/ul>
&lt;p>Langfuse bootstrap (ClickHouse especially) takes 5–15 minutes on first run. The script prints progress and continues.&lt;/p>
&lt;h2 id="wiring-observability-minimal-manual-step">Wiring Observability (Minimal Manual Step)&lt;/h2>
&lt;p>Once &lt;code>langfuse-web&lt;/code> is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n langfuse svc/langfuse-web 3000:3000 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Open http://localhost:3000, create a project, copy Public + Secret keys&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">LANGFUSE_PUBLIC_KEY&lt;/span>&lt;span class="o">=&lt;/span>pk-lf-...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">LANGFUSE_SECRET_KEY&lt;/span>&lt;span class="o">=&lt;/span>sk-lf-...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./configure-observability.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This single script:&lt;/p>
&lt;ul>
&lt;li>Computes the correct Basic Auth header Langfuse expects&lt;/li>
&lt;li>Applies the complete &lt;code>AgentgatewayParameters&lt;/code> resource with:
&lt;ul>
&lt;li>OTLP/HTTP endpoint pointing at the in-cluster Langfuse service&lt;/li>
&lt;li>Proper Authorization header&lt;/li>
&lt;li>Full field mappings for &lt;code>gen_ai.prompt&lt;/code>, &lt;code>gen_ai.completion&lt;/code>, streaming flags, &lt;code>user.id&lt;/code>, &lt;code>session.id&lt;/code>, client IP, and environment tags&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>The agentgateway controller immediately reconciles the change and pushes the new tracing config to all data-plane proxies&lt;/li>
&lt;/ul>
&lt;p>From this point forward, &lt;strong>agentgateway&lt;/strong> is responsible for capturing cost-relevant telemetry on every request and streaming it to Langfuse in real time. No additional instrumentation, sidecars, or log shipping is required.&lt;/p>
&lt;h2 id="how-agentgateway-manages-costs--sends-logs-to-langfuse">How agentgateway Manages Costs &amp;amp; Sends Logs to Langfuse&lt;/h2>
&lt;p>&lt;strong>agentgateway&lt;/strong> acts as your cost control plane:&lt;/p>
&lt;ol>
&lt;li>Every OpenAI-compatible request hits the Gateway first&lt;/li>
&lt;li>It extracts and enriches telemetry using the GenAI semantic conventions&lt;/li>
&lt;li>It streams complete traces (including full prompt/completion bodies) over OTLP/HTTP directly to Langfuse’s &lt;code>/api/public/otel/v1/traces&lt;/code> endpoint&lt;/li>
&lt;li>Langfuse turns these traces into first-class “Generations” with token accounting&lt;/li>
&lt;/ol>
&lt;p>Key data &lt;strong>agentgateway&lt;/strong> sends for cost management:&lt;/p>
&lt;ul>
&lt;li>&lt;code>gen_ai.request.model&lt;/code>&lt;/li>
&lt;li>&lt;code>gen_ai.usage.input_tokens&lt;/code> / &lt;code>gen_ai.usage.output_tokens&lt;/code>&lt;/li>
&lt;li>Full &lt;code>gen_ai.prompt&lt;/code> and &lt;code>gen_ai.completion&lt;/code>&lt;/li>
&lt;li>&lt;code>user.id&lt;/code> and &lt;code>session.id&lt;/code> (via request headers for attribution and filtering)&lt;/li>
&lt;li>Latency, streaming status, client IP, and environment tags&lt;/li>
&lt;/ul>
&lt;p>Once pricing is configured in Langfuse, you get accurate per-request and aggregated USD costs.&lt;/p>
&lt;h2 id="test--observe-costs">Test &amp;amp; Observe Costs&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh --users &lt;span class="c1"># realistic multi-user traffic with attribution headers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh &lt;span class="s2">&amp;#34;Explain Kubernetes Gateway API in one paragraph.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>agentgateway&lt;/strong> captures and forwards:&lt;/p>
&lt;ul>
&lt;li>Exact model name and provider&lt;/li>
&lt;li>Input and output token counts (&lt;code>gen_ai.usage.input_tokens&lt;/code>, &lt;code>gen_ai.usage.output_tokens&lt;/code>)&lt;/li>
&lt;li>Full prompt and completion bodies&lt;/li>
&lt;li>Latency and streaming status&lt;/li>
&lt;li>User and session attribution (via &lt;code>x-user-id&lt;/code> / &lt;code>x-session-id&lt;/code> headers)&lt;/li>
&lt;/ul>
&lt;p>Open Langfuse → Traces / Generations. You’ll immediately see rich entries.&lt;/p>
&lt;p>Then go to &lt;strong>Project → Settings → Models&lt;/strong> and add pricing for your model (e.g. Qwen). Langfuse automatically calculates real USD costs per generation and aggregates them into spend dashboards. This is how you turn raw token logs into actionable cost management.&lt;/p>
&lt;h2 id="why-this-approach-wins">Why This Approach Wins&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Zero collector complexity&lt;/strong> — agentgateway speaks Langfuse’s OTLP endpoint natively&lt;/li>
&lt;li>&lt;strong>Idempotent &amp;amp; reproducible&lt;/strong> — re-run deploy.sh safely&lt;/li>
&lt;li>&lt;strong>Local-model friendly&lt;/strong> — hostOverride pattern works cleanly inside kind&lt;/li>
&lt;li>&lt;strong>Production-ready patterns&lt;/strong> — the same tracing config translates directly to real clusters&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./cleanup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Removes the cluster and all resources.&lt;/p>
&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>Clone the demo and run it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">https://github.com/sebbycorp/agentgateway-demos/tree/main/09-k8s-langfuse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is currently the fastest way to stand up a complete, observable AI gateway stack on Kubernetes for cost tracking and debugging.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Related reading:&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="2026-06-10-self-hosted-langfuse-docker-kagent-integration.md">Self-Hosted Langfuse with Docker + kagent Integration&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-14-llm-observability-agentgateway-langfuse.md">LLM Observability with agentgateway + Langfuse&lt;/a>&lt;/li>
&lt;li>agentgateway Kubernetes docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest">https://agentgateway.dev/docs/kubernetes/latest&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>Running production-grade LLM gateways with full observability usually involves many manual steps across Helm charts, CRDs, tracing configuration, and UI key management.&lt;/p>
&lt;p>The &lt;strong>agentgateway-demos/09-k8s-langfuse&lt;/strong> demo eliminates almost all of that friction by making &lt;strong>agentgateway the central cost-control and observability plane&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>agentgateway&lt;/strong> sits in front of your models, inspects every request/response, emits rich GenAI OpenTelemetry traces (model name, input/output tokens, full prompt + completion, latency, user/session attribution), and forwards them directly to Langfuse. Once you set per-model pricing in Langfuse, you get automatic USD cost tracking, spend dashboards, and usage analytics — all without any external SaaS or manual log shipping.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;p>A single &lt;code>./deploy.sh&lt;/code> that provisions:&lt;/p>
&lt;ul>
&lt;li>A dedicated kind cluster (&lt;code>agw-k8s-langfuse&lt;/code>)&lt;/li>
&lt;li>Full self-hosted Langfuse (PostgreSQL + ClickHouse + Redis + web/worker) via the official Helm chart, tuned for kind&lt;/li>
&lt;li>agentgateway CRDs, controller, GatewayClass, and proxy (v1.1.0)&lt;/li>
&lt;li>Pre-configured &lt;code>AgentgatewayBackend&lt;/code> + &lt;code>HTTPRoute&lt;/code> routing &lt;code>/v1/*&lt;/code> to your local OpenAI-compatible model (e.g. Qwen running on your host)&lt;/li>
&lt;li>Direct OTLP/HTTP tracing from agentgateway → Langfuse (no collector sidecar required)&lt;/li>
&lt;/ul>
&lt;p>After one follow-up script with your project keys, &lt;strong>agentgateway&lt;/strong> automatically sends every LLM call to Langfuse with the data needed for real cost management.&lt;/p>
&lt;h2 id="quick-start-the-automated-path">Quick Start (The Automated Path)&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/09-k8s-langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./deploy.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The script handles:&lt;/p>
&lt;ul>
&lt;li>kind cluster creation&lt;/li>
&lt;li>Gateway API CRDs&lt;/li>
&lt;li>Langfuse Helm install + phased readiness waits (databases first)&lt;/li>
&lt;li>agentgateway controller + CRDs&lt;/li>
&lt;li>Base &lt;code>AgentgatewayParameters&lt;/code>&lt;/li>
&lt;li>Gateway + &lt;code>spark&lt;/code> backend + &lt;code>spark-route&lt;/code> HTTPRoute&lt;/li>
&lt;/ul>
&lt;p>Langfuse bootstrap (ClickHouse especially) takes 5–15 minutes on first run. The script prints progress and continues.&lt;/p>
&lt;h2 id="wiring-observability-minimal-manual-step">Wiring Observability (Minimal Manual Step)&lt;/h2>
&lt;p>Once &lt;code>langfuse-web&lt;/code> is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n langfuse svc/langfuse-web 3000:3000 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Open http://localhost:3000, create a project, copy Public + Secret keys&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">LANGFUSE_PUBLIC_KEY&lt;/span>&lt;span class="o">=&lt;/span>pk-lf-...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">LANGFUSE_SECRET_KEY&lt;/span>&lt;span class="o">=&lt;/span>sk-lf-...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./configure-observability.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This single script:&lt;/p>
&lt;ul>
&lt;li>Computes the correct Basic Auth header Langfuse expects&lt;/li>
&lt;li>Applies the complete &lt;code>AgentgatewayParameters&lt;/code> resource with:
&lt;ul>
&lt;li>OTLP/HTTP endpoint pointing at the in-cluster Langfuse service&lt;/li>
&lt;li>Proper Authorization header&lt;/li>
&lt;li>Full field mappings for &lt;code>gen_ai.prompt&lt;/code>, &lt;code>gen_ai.completion&lt;/code>, streaming flags, &lt;code>user.id&lt;/code>, &lt;code>session.id&lt;/code>, client IP, and environment tags&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>The agentgateway controller immediately reconciles the change and pushes the new tracing config to all data-plane proxies&lt;/li>
&lt;/ul>
&lt;p>From this point forward, &lt;strong>agentgateway&lt;/strong> is responsible for capturing cost-relevant telemetry on every request and streaming it to Langfuse in real time. No additional instrumentation, sidecars, or log shipping is required.&lt;/p>
&lt;h2 id="how-agentgateway-manages-costs--sends-logs-to-langfuse">How agentgateway Manages Costs &amp;amp; Sends Logs to Langfuse&lt;/h2>
&lt;p>&lt;strong>agentgateway&lt;/strong> acts as your cost control plane:&lt;/p>
&lt;ol>
&lt;li>Every OpenAI-compatible request hits the Gateway first&lt;/li>
&lt;li>It extracts and enriches telemetry using the GenAI semantic conventions&lt;/li>
&lt;li>It streams complete traces (including full prompt/completion bodies) over OTLP/HTTP directly to Langfuse’s &lt;code>/api/public/otel/v1/traces&lt;/code> endpoint&lt;/li>
&lt;li>Langfuse turns these traces into first-class “Generations” with token accounting&lt;/li>
&lt;/ol>
&lt;p>Key data &lt;strong>agentgateway&lt;/strong> sends for cost management:&lt;/p>
&lt;ul>
&lt;li>&lt;code>gen_ai.request.model&lt;/code>&lt;/li>
&lt;li>&lt;code>gen_ai.usage.input_tokens&lt;/code> / &lt;code>gen_ai.usage.output_tokens&lt;/code>&lt;/li>
&lt;li>Full &lt;code>gen_ai.prompt&lt;/code> and &lt;code>gen_ai.completion&lt;/code>&lt;/li>
&lt;li>&lt;code>user.id&lt;/code> and &lt;code>session.id&lt;/code> (via request headers for attribution and filtering)&lt;/li>
&lt;li>Latency, streaming status, client IP, and environment tags&lt;/li>
&lt;/ul>
&lt;p>Once pricing is configured in Langfuse, you get accurate per-request and aggregated USD costs.&lt;/p>
&lt;h2 id="test--observe-costs">Test &amp;amp; Observe Costs&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:80 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh --users &lt;span class="c1"># realistic multi-user traffic with attribution headers&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh &lt;span class="s2">&amp;#34;Explain Kubernetes Gateway API in one paragraph.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>agentgateway&lt;/strong> captures and forwards:&lt;/p>
&lt;ul>
&lt;li>Exact model name and provider&lt;/li>
&lt;li>Input and output token counts (&lt;code>gen_ai.usage.input_tokens&lt;/code>, &lt;code>gen_ai.usage.output_tokens&lt;/code>)&lt;/li>
&lt;li>Full prompt and completion bodies&lt;/li>
&lt;li>Latency and streaming status&lt;/li>
&lt;li>User and session attribution (via &lt;code>x-user-id&lt;/code> / &lt;code>x-session-id&lt;/code> headers)&lt;/li>
&lt;/ul>
&lt;p>Open Langfuse → Traces / Generations. You’ll immediately see rich entries.&lt;/p>
&lt;p>Then go to &lt;strong>Project → Settings → Models&lt;/strong> and add pricing for your model (e.g. Qwen). Langfuse automatically calculates real USD costs per generation and aggregates them into spend dashboards. This is how you turn raw token logs into actionable cost management.&lt;/p>
&lt;h2 id="why-this-approach-wins">Why This Approach Wins&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Zero collector complexity&lt;/strong> — agentgateway speaks Langfuse’s OTLP endpoint natively&lt;/li>
&lt;li>&lt;strong>Idempotent &amp;amp; reproducible&lt;/strong> — re-run deploy.sh safely&lt;/li>
&lt;li>&lt;strong>Local-model friendly&lt;/strong> — hostOverride pattern works cleanly inside kind&lt;/li>
&lt;li>&lt;strong>Production-ready patterns&lt;/strong> — the same tracing config translates directly to real clusters&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./cleanup.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Removes the cluster and all resources.&lt;/p>
&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>Clone the demo and run it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">https://github.com/sebbycorp/agentgateway-demos/tree/main/09-k8s-langfuse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is currently the fastest way to stand up a complete, observable AI gateway stack on Kubernetes for cost tracking and debugging.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Related reading:&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="2026-06-10-self-hosted-langfuse-docker-kagent-integration.md">Self-Hosted Langfuse with Docker + kagent Integration&lt;/a>&lt;/li>
&lt;li>&lt;a href="2026-02-14-llm-observability-agentgateway-langfuse.md">LLM Observability with agentgateway + Langfuse&lt;/a>&lt;/li>
&lt;li>agentgateway Kubernetes docs: &lt;a href="https://agentgateway.dev/docs/kubernetes/latest">https://agentgateway.dev/docs/kubernetes/latest&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>agentgateway Standalone → Langfuse: Direct OTLP Tracing (No Collector)</title><link>https://maniak.io/articles/2026-06-10-agentgateway-standalone-langfuse-direct-otlp/</link><pubDate>Wed, 10 Jun 2026 23:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-06-10-agentgateway-standalone-langfuse-direct-otlp/</guid><description>&lt;p>I&amp;rsquo;ve already covered the &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">production OTel Collector pattern&lt;/a> for shipping agentgateway traces to Langfuse. This is the opposite end of the spectrum: the &lt;strong>absolute minimum&lt;/strong> that works.&lt;/p>
&lt;p>No collector. No sidecar. No extra containers. Just the &lt;strong>agentgateway binary running standalone&lt;/strong>, pointing its OTLP exporter directly at a self-hosted Langfuse on another VM. If you want LLM tracing on your laptop or a single box in about five minutes, this is the path.&lt;/p>
&lt;blockquote>
&lt;p>Full working config: &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/08-standalone-langfuse">&lt;code>sebbycorp/agentgateway-demos/08-standalone-langfuse&lt;/code>&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="the-data-path">The data path&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">your app ──▶ agentgateway (:3000) ──▶ vLLM / Qwen backend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── OTLP/HTTP traces ──▶ Langfuse VM (:3000/api/public/otel)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the whole thing. agentgateway natively emits OpenTelemetry traces using the &lt;strong>GenAI semantic conventions&lt;/strong> (&lt;code>gen_ai.request.model&lt;/code>, &lt;code>gen_ai.usage.input_tokens&lt;/code>, &lt;code>gen_ai.operation.name&lt;/code>, …). Langfuse exposes a built-in OTLP receiver at &lt;code>/api/public/otel&lt;/code>, so the gateway talks to it directly — no translation layer in between.&lt;/p>
&lt;p>The only requirement: the box running agentgateway needs network access to the Langfuse host on port 3000.&lt;/p>
&lt;hr>
&lt;h2 id="why-no-collector">Why no collector?&lt;/h2>
&lt;p>The collector pattern earns its keep when you need gRPC, batching, fan-out to multiple backends, or central enrichment at scale. For a single gateway sending to a single Langfuse, all of that is overhead.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Standalone (this guide)&lt;/th>
&lt;th>OTel Collector&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Moving parts&lt;/td>
&lt;td>1 binary&lt;/td>
&lt;td>gateway + collector deployment&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Protocol&lt;/td>
&lt;td>OTLP/HTTP only&lt;/td>
&lt;td>gRPC or HTTP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Auth handling&lt;/td>
&lt;td>inline header (substituted at launch)&lt;/td>
&lt;td>collector holds the secret&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Best for&lt;/td>
&lt;td>laptops, single VM, demos, MVP&lt;/td>
&lt;td>clusters, multi-backend, prod&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you outgrow it, the &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">collector article&lt;/a> is the next step. For now, keep it simple.&lt;/p>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>The &lt;code>agentgateway&lt;/code> binary on your &lt;code>PATH&lt;/code> (or sitting next to the config).&lt;/li>
&lt;li>A reachable Langfuse instance. Mine is self-hosted at &lt;code>http://172.16.10.112:3000&lt;/code>.&lt;/li>
&lt;li>A Langfuse &lt;strong>public key&lt;/strong> and &lt;strong>secret key&lt;/strong> (Project Settings → API Keys).&lt;/li>
&lt;li>An LLM backend. I&amp;rsquo;m pointing at a local &lt;strong>vLLM serving &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>&lt;/strong> at &lt;code>172.16.10.173:8000&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Quick reachability check before you start:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -I http://172.16.10.112:3000 &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Cannot reach Langfuse VM&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="the-config">The config&lt;/h2>
&lt;p>Everything lives in one &lt;code>config.yaml&lt;/code>. Here&amp;rsquo;s the tracing block — the part that matters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Langfuse OTLP HTTP endpoint (gRPC is NOT supported by Langfuse ingest).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># EU cloud: https://cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># US cloud: https://us.cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Self-hosted: http://&amp;lt;host&amp;gt;:3000/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://172.16.10.112:3000/api/public/otel/v1/traces&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># IMPORTANT: force HTTP (protobuf/json). The default is grpc.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;Basic ${LANGFUSE_AUTH_STRING}&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">x-langfuse-ingestion-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;4&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 1.0 / true for dev. Lower this in prod (e.g. 0.1).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two things trip people up here, both of them about &lt;strong>CEL&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>otlpProtocol: http&lt;/code> is mandatory.&lt;/strong> agentgateway defaults to gRPC, and Langfuse&amp;rsquo;s ingest endpoint does not speak gRPC OTLP. Leave it on the default and traces silently never arrive.&lt;/li>
&lt;li>&lt;strong>Header values are CEL expressions, not raw strings.&lt;/strong> That&amp;rsquo;s why the values are double-quoted: &lt;code>'&amp;quot;Basic ${...}&amp;quot;'&lt;/code>. The outer quotes are YAML; the inner quotes make it a CEL &lt;em>string literal&lt;/em>. Drop the inner quotes and the gateway tries to evaluate &lt;code>Basic ...&lt;/code> as an expression and fails to parse.&lt;/li>
&lt;/ol>
&lt;h3 id="enriching-the-traces">Enriching the traces&lt;/h3>
&lt;p>The default GenAI spans are good, but a few extra &lt;code>fields&lt;/code> turn them into something genuinely useful in Langfuse:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># The actual conversation → Langfuse maps these to trace input/output.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># string() casts raw body bytes to text (else you get base64).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;string(request.body)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;string(response.body)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.stream&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;json(request.body).stream&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Attribution / multi-tenant — pulled from request headers.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user.id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.headers[&amp;#34;x-user-id&amp;#34;]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">session.id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.headers[&amp;#34;x-session-id&amp;#34;]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;local-dev&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">client.ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;source.address&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>user.id&lt;/code> line is the one to notice — it&amp;rsquo;s what produces the &lt;strong>per-user cost breakdown&lt;/strong> you&amp;rsquo;ll see in Langfuse later. No app changes required; the gateway is the instrumentation point.&lt;/p>
&lt;h3 id="the-backend">The backend&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">spark-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">spark-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">spark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostOverride&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;172.16.10.173:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This uses the &lt;code>openAI&lt;/code> provider type (vLLM speaks the OpenAI API) with &lt;code>hostOverride&lt;/code> pointing at the local vLLM server.&lt;/p>
&lt;hr>
&lt;h2 id="the-secret-substitution-gotcha">The secret-substitution gotcha&lt;/h2>
&lt;p>You&amp;rsquo;ll notice the config contains the literal placeholder &lt;code>${LANGFUSE_AUTH_STRING}&lt;/code>. &lt;strong>agentgateway does not expand environment variables when it reads YAML.&lt;/strong> If you &lt;code>source .env&lt;/code> and launch the binary directly, it takes the literal string &lt;code>${LANGFUSE_AUTH_STRING}&lt;/code>, tries to parse it as CEL, and crashes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Error: parse: ... Syntax error: token recognition error at: &amp;#39;$&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| Basic ${LANGFUSE_AUTH_STRING}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The fix is a tiny wrapper that substitutes the value into a temp file right before launch — keeping the real secret out of the committed config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>dirname &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">BASH_SOURCE&lt;/span>&lt;span class="p">[0]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Load secrets from .env (gitignored)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> &lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/.env&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Substitute into a throwaway config; clean it up on exit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">CONFIG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>mktemp&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">trap&lt;/span> &lt;span class="s1">&amp;#39;rm -f &amp;#34;$CONFIG_FILE&amp;#34;&amp;#39;&lt;/span> EXIT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nb">command&lt;/span> -v envsubst &amp;gt;/dev/null 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>1&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> envsubst &amp;lt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/config.yaml&amp;#34;&lt;/span> &amp;gt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONFIG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sed &lt;span class="s2">&amp;#34;s|\${LANGFUSE_AUTH_STRING}|&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LANGFUSE_AUTH_STRING&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">|g&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/config.yaml&amp;#34;&lt;/span> &amp;gt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONFIG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> agentgateway -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONFIG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$@&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Build the auth string once and drop it in &lt;code>.env&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LANGFUSE_PUBLIC_KEY&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$LANGFUSE_SECRET_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> base64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># → put the result in .env as LANGFUSE_AUTH_STRING=...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is the same pattern you use for any proxy that doesn&amp;rsquo;t do env expansion natively (Envoy-style tools). &lt;code>.env&lt;/code> stays gitignored; &lt;code>config.yaml&lt;/code> stays clean.&lt;/p>
&lt;hr>
&lt;h2 id="run-it">Run it&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x run.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./run.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see the startup logs print the substituted tracing block — the &lt;code>Authorization&lt;/code> header should now read &lt;code>Basic cG...&lt;/code>, &lt;strong>not&lt;/strong> the &lt;code>${LANGFUSE_AUTH_STRING}&lt;/code> placeholder. That one line is your fastest confirmation the substitution worked.&lt;/p>
&lt;hr>
&lt;h2 id="verify-the-gateway-in-the-ui">Verify the gateway in the UI&lt;/h2>
&lt;p>agentgateway ships a local UI. Open it and you should see the bind, the route, and the AI backend all wired to port 3000.&lt;/p>
&lt;p>&lt;strong>Listeners&lt;/strong> — one HTTP listener (&lt;code>spark-http&lt;/code>) bound to port 3000:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-listeners.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-listeners.png" alt="agentgateway Port Binds &amp;amp;amp; Listeners showing the spark-http HTTP listener on port 3000" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;strong>Routes&lt;/strong> — &lt;code>spark-route&lt;/code> attached to the &lt;code>spark-http&lt;/code> listener, matching all hosts and paths:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-routes.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-routes.png" alt="agentgateway Routes showing spark-route on the spark-http listener" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;strong>Backends&lt;/strong> — the &lt;code>spark&lt;/code> AI backend, provider OpenAI-compatible, model &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-backends.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-backends.png" alt="agentgateway Backends showing the spark AI backend using the Qwen model" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="send-some-traffic">Send some traffic&lt;/h2>
&lt;p>Anything that hits the OpenAI-compatible endpoint on port 3000 gets traced. The key is passing &lt;code>x-user-id&lt;/code> and &lt;code>x-session-id&lt;/code> headers so the gateway can attribute each trace:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST &lt;span class="s2">&amp;#34;http://localhost:3000/v1/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-user-id: alice&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-session-id: demo-session-1&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Explain OTLP in one sentence.&amp;#34;}],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;temperature&amp;#34;: 0.7,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;stream&amp;#34;: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The repo&amp;rsquo;s &lt;code>test.sh&lt;/code> automates this — it loops over a handful of demo users (&lt;strong>alice&lt;/strong>, &lt;strong>bob&lt;/strong>, &lt;strong>carol&lt;/strong>, &lt;strong>dave&lt;/strong>, &lt;strong>erin&lt;/strong>) with different traffic weights and supports streaming via &lt;code>--stream&lt;/code>, which is exactly how you generate the per-user breakdown below:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x test.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh &lt;span class="c1"># non-streaming&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh --stream &lt;span class="c1"># SSE streaming&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="see-it-in-langfuse">See it in Langfuse&lt;/h2>
&lt;p>Open your Langfuse project. Within a second or two of sending traffic (that&amp;rsquo;s what &lt;code>x-langfuse-ingestion-version: &amp;quot;4&amp;quot;&lt;/code> buys you — real-time previews), traces start landing. Because the gateway emits proper GenAI conventions, Langfuse renders them as full &lt;strong>generations&lt;/strong> with model, token counts, latency, and &lt;strong>cost&lt;/strong> — even for a local Qwen model.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/langfuse-dashboard.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/langfuse-dashboard.png" alt="Langfuse dashboard showing 25 traces tracked and per-user token cost: alice, demo-user-42, bob, carol, dave, totaling $0.12699" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>That &lt;strong>User consumption&lt;/strong> panel is the payoff of the &lt;code>user.id&lt;/code> field in the config — total spend broken out per user (&lt;code>alice&lt;/code> $0.04328, &lt;code>bob&lt;/code> $0.02147, and so on), all from a gateway that the LLM apps never had to be aware of.&lt;/p>
&lt;hr>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>Traces never appear.&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Confirm the gateway box can reach Langfuse: &lt;code>curl -I http://172.16.10.112:3000&lt;/code>.&lt;/li>
&lt;li>Check the startup logs show the &lt;em>real&lt;/em> &lt;code>Authorization: Basic cG...&lt;/code> header, not the &lt;code>${...}&lt;/code> placeholder. If it&amp;rsquo;s still the placeholder, you launched the binary directly instead of via &lt;code>run.sh&lt;/code>.&lt;/li>
&lt;li>Make sure &lt;code>otlpProtocol: http&lt;/code> is set — gRPC silently fails against Langfuse.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>&lt;code>Syntax error: token recognition error at: '$'&lt;/code>&lt;/strong> — the literal placeholder reached the binary. Use &lt;code>run.sh&lt;/code> (or &lt;code>envsubst&lt;/code>) so the secret is substituted before launch.&lt;/p>
&lt;p>&lt;strong>Inputs/outputs show as base64 in Langfuse&lt;/strong> — wrap the body fields in &lt;code>string(...)&lt;/code>, as in the &lt;code>fields.add&lt;/code> block above.&lt;/p>
&lt;hr>
&lt;h2 id="when-to-graduate-to-a-collector">When to graduate to a collector&lt;/h2>
&lt;p>Stay standalone for laptops, single VMs, demos, and MVPs — one binary, one config, done. Reach for the &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">OTel Collector pattern&lt;/a> when you need gRPC from the gateway, batching and retries under load, fan-out to more than one observability backend, or you simply don&amp;rsquo;t want the Langfuse secret living anywhere near the gateway process.&lt;/p>
&lt;p>Same traces, same GenAI conventions — just a heavier, more flexible delivery path. Start here; grow into that.&lt;/p>
&lt;hr>
&lt;h2 id="get-the-code">Get the code&lt;/h2>
&lt;p>The complete, runnable setup — &lt;code>config.yaml&lt;/code>, &lt;code>run.sh&lt;/code>, and &lt;code>test.sh&lt;/code> — lives in my demos repo:&lt;/p>
&lt;p>&lt;strong>→ &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/08-standalone-langfuse">github.com/sebbycorp/agentgateway-demos/tree/main/08-standalone-langfuse&lt;/a>&lt;/strong>&lt;/p>
&lt;p>Clone it, drop your keys into &lt;code>.env&lt;/code>, and &lt;code>./run.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/08-standalone-langfuse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description><content:encoded>&lt;p>I&amp;rsquo;ve already covered the &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">production OTel Collector pattern&lt;/a> for shipping agentgateway traces to Langfuse. This is the opposite end of the spectrum: the &lt;strong>absolute minimum&lt;/strong> that works.&lt;/p>
&lt;p>No collector. No sidecar. No extra containers. Just the &lt;strong>agentgateway binary running standalone&lt;/strong>, pointing its OTLP exporter directly at a self-hosted Langfuse on another VM. If you want LLM tracing on your laptop or a single box in about five minutes, this is the path.&lt;/p>
&lt;blockquote>
&lt;p>Full working config: &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/08-standalone-langfuse">&lt;code>sebbycorp/agentgateway-demos/08-standalone-langfuse&lt;/code>&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="the-data-path">The data path&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">your app ──▶ agentgateway (:3000) ──▶ vLLM / Qwen backend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── OTLP/HTTP traces ──▶ Langfuse VM (:3000/api/public/otel)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s the whole thing. agentgateway natively emits OpenTelemetry traces using the &lt;strong>GenAI semantic conventions&lt;/strong> (&lt;code>gen_ai.request.model&lt;/code>, &lt;code>gen_ai.usage.input_tokens&lt;/code>, &lt;code>gen_ai.operation.name&lt;/code>, …). Langfuse exposes a built-in OTLP receiver at &lt;code>/api/public/otel&lt;/code>, so the gateway talks to it directly — no translation layer in between.&lt;/p>
&lt;p>The only requirement: the box running agentgateway needs network access to the Langfuse host on port 3000.&lt;/p>
&lt;hr>
&lt;h2 id="why-no-collector">Why no collector?&lt;/h2>
&lt;p>The collector pattern earns its keep when you need gRPC, batching, fan-out to multiple backends, or central enrichment at scale. For a single gateway sending to a single Langfuse, all of that is overhead.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Standalone (this guide)&lt;/th>
&lt;th>OTel Collector&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Moving parts&lt;/td>
&lt;td>1 binary&lt;/td>
&lt;td>gateway + collector deployment&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Protocol&lt;/td>
&lt;td>OTLP/HTTP only&lt;/td>
&lt;td>gRPC or HTTP&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Auth handling&lt;/td>
&lt;td>inline header (substituted at launch)&lt;/td>
&lt;td>collector holds the secret&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Best for&lt;/td>
&lt;td>laptops, single VM, demos, MVP&lt;/td>
&lt;td>clusters, multi-backend, prod&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you outgrow it, the &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">collector article&lt;/a> is the next step. For now, keep it simple.&lt;/p>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>The &lt;code>agentgateway&lt;/code> binary on your &lt;code>PATH&lt;/code> (or sitting next to the config).&lt;/li>
&lt;li>A reachable Langfuse instance. Mine is self-hosted at &lt;code>http://172.16.10.112:3000&lt;/code>.&lt;/li>
&lt;li>A Langfuse &lt;strong>public key&lt;/strong> and &lt;strong>secret key&lt;/strong> (Project Settings → API Keys).&lt;/li>
&lt;li>An LLM backend. I&amp;rsquo;m pointing at a local &lt;strong>vLLM serving &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>&lt;/strong> at &lt;code>172.16.10.173:8000&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>Quick reachability check before you start:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -I http://172.16.10.112:3000 &lt;span class="o">||&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Cannot reach Langfuse VM&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="the-config">The config&lt;/h2>
&lt;p>Everything lives in one &lt;code>config.yaml&lt;/code>. Here&amp;rsquo;s the tracing block — the part that matters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Langfuse OTLP HTTP endpoint (gRPC is NOT supported by Langfuse ingest).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># EU cloud: https://cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># US cloud: https://us.cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Self-hosted: http://&amp;lt;host&amp;gt;:3000/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://172.16.10.112:3000/api/public/otel/v1/traces&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># IMPORTANT: force HTTP (protobuf/json). The default is grpc.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;Basic ${LANGFUSE_AUTH_STRING}&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">x-langfuse-ingestion-version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;4&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># 1.0 / true for dev. Lower this in prod (e.g. 0.1).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two things trip people up here, both of them about &lt;strong>CEL&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>otlpProtocol: http&lt;/code> is mandatory.&lt;/strong> agentgateway defaults to gRPC, and Langfuse&amp;rsquo;s ingest endpoint does not speak gRPC OTLP. Leave it on the default and traces silently never arrive.&lt;/li>
&lt;li>&lt;strong>Header values are CEL expressions, not raw strings.&lt;/strong> That&amp;rsquo;s why the values are double-quoted: &lt;code>'&amp;quot;Basic ${...}&amp;quot;'&lt;/code>. The outer quotes are YAML; the inner quotes make it a CEL &lt;em>string literal&lt;/em>. Drop the inner quotes and the gateway tries to evaluate &lt;code>Basic ...&lt;/code> as an expression and fails to parse.&lt;/li>
&lt;/ol>
&lt;h3 id="enriching-the-traces">Enriching the traces&lt;/h3>
&lt;p>The default GenAI spans are good, but a few extra &lt;code>fields&lt;/code> turn them into something genuinely useful in Langfuse:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># The actual conversation → Langfuse maps these to trace input/output.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># string() casts raw body bytes to text (else you get base64).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;string(request.body)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;string(response.body)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.stream&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;json(request.body).stream&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Attribution / multi-tenant — pulled from request headers.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user.id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.headers[&amp;#34;x-user-id&amp;#34;]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">session.id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.headers[&amp;#34;x-session-id&amp;#34;]&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;local-dev&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">client.ip&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;source.address&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>user.id&lt;/code> line is the one to notice — it&amp;rsquo;s what produces the &lt;strong>per-user cost breakdown&lt;/strong> you&amp;rsquo;ll see in Langfuse later. No app changes required; the gateway is the instrumentation point.&lt;/p>
&lt;h3 id="the-backend">The backend&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">spark-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">spark-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">spark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostOverride&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;172.16.10.173:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This uses the &lt;code>openAI&lt;/code> provider type (vLLM speaks the OpenAI API) with &lt;code>hostOverride&lt;/code> pointing at the local vLLM server.&lt;/p>
&lt;hr>
&lt;h2 id="the-secret-substitution-gotcha">The secret-substitution gotcha&lt;/h2>
&lt;p>You&amp;rsquo;ll notice the config contains the literal placeholder &lt;code>${LANGFUSE_AUTH_STRING}&lt;/code>. &lt;strong>agentgateway does not expand environment variables when it reads YAML.&lt;/strong> If you &lt;code>source .env&lt;/code> and launch the binary directly, it takes the literal string &lt;code>${LANGFUSE_AUTH_STRING}&lt;/code>, tries to parse it as CEL, and crashes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Error: parse: ... Syntax error: token recognition error at: &amp;#39;$&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">| Basic ${LANGFUSE_AUTH_STRING}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The fix is a tiny wrapper that substitutes the value into a temp file right before launch — keeping the real secret out of the committed config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">set&lt;/span> -euo pipefail
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>dirname &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">BASH_SOURCE&lt;/span>&lt;span class="p">[0]&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Load secrets from .env (gitignored)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">set&lt;/span> -a&lt;span class="p">;&lt;/span> &lt;span class="nb">source&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/.env&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">set&lt;/span> +a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Substitute into a throwaway config; clean it up on exit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">CONFIG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>mktemp&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">trap&lt;/span> &lt;span class="s1">&amp;#39;rm -f &amp;#34;$CONFIG_FILE&amp;#34;&amp;#39;&lt;/span> EXIT
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nb">command&lt;/span> -v envsubst &amp;gt;/dev/null 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>1&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> envsubst &amp;lt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/config.yaml&amp;#34;&lt;/span> &amp;gt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONFIG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sed &lt;span class="s2">&amp;#34;s|\${LANGFUSE_AUTH_STRING}|&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LANGFUSE_AUTH_STRING&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">|g&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">SCRIPT_DIR&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/config.yaml&amp;#34;&lt;/span> &amp;gt; &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONFIG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> agentgateway -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$CONFIG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$@&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Build the auth string once and drop it in &lt;code>.env&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LANGFUSE_PUBLIC_KEY&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$LANGFUSE_SECRET_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> base64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># → put the result in .env as LANGFUSE_AUTH_STRING=...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is the same pattern you use for any proxy that doesn&amp;rsquo;t do env expansion natively (Envoy-style tools). &lt;code>.env&lt;/code> stays gitignored; &lt;code>config.yaml&lt;/code> stays clean.&lt;/p>
&lt;hr>
&lt;h2 id="run-it">Run it&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x run.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./run.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see the startup logs print the substituted tracing block — the &lt;code>Authorization&lt;/code> header should now read &lt;code>Basic cG...&lt;/code>, &lt;strong>not&lt;/strong> the &lt;code>${LANGFUSE_AUTH_STRING}&lt;/code> placeholder. That one line is your fastest confirmation the substitution worked.&lt;/p>
&lt;hr>
&lt;h2 id="verify-the-gateway-in-the-ui">Verify the gateway in the UI&lt;/h2>
&lt;p>agentgateway ships a local UI. Open it and you should see the bind, the route, and the AI backend all wired to port 3000.&lt;/p>
&lt;p>&lt;strong>Listeners&lt;/strong> — one HTTP listener (&lt;code>spark-http&lt;/code>) bound to port 3000:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-listeners.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-listeners.png" alt="agentgateway Port Binds &amp;amp;amp; Listeners showing the spark-http HTTP listener on port 3000" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;strong>Routes&lt;/strong> — &lt;code>spark-route&lt;/code> attached to the &lt;code>spark-http&lt;/code> listener, matching all hosts and paths:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-routes.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-routes.png" alt="agentgateway Routes showing spark-route on the spark-http listener" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;strong>Backends&lt;/strong> — the &lt;code>spark&lt;/code> AI backend, provider OpenAI-compatible, model &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-backends.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/agentgateway-backends.png" alt="agentgateway Backends showing the spark AI backend using the Qwen model" loading="lazy">
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h2 id="send-some-traffic">Send some traffic&lt;/h2>
&lt;p>Anything that hits the OpenAI-compatible endpoint on port 3000 gets traced. The key is passing &lt;code>x-user-id&lt;/code> and &lt;code>x-session-id&lt;/code> headers so the gateway can attribute each trace:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST &lt;span class="s2">&amp;#34;http://localhost:3000/v1/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-user-id: alice&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-session-id: demo-session-1&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Explain OTLP in one sentence.&amp;#34;}],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;temperature&amp;#34;: 0.7,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;stream&amp;#34;: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The repo&amp;rsquo;s &lt;code>test.sh&lt;/code> automates this — it loops over a handful of demo users (&lt;strong>alice&lt;/strong>, &lt;strong>bob&lt;/strong>, &lt;strong>carol&lt;/strong>, &lt;strong>dave&lt;/strong>, &lt;strong>erin&lt;/strong>) with different traffic weights and supports streaming via &lt;code>--stream&lt;/code>, which is exactly how you generate the per-user breakdown below:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">chmod +x test.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh &lt;span class="c1"># non-streaming&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test.sh --stream &lt;span class="c1"># SSE streaming&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="see-it-in-langfuse">See it in Langfuse&lt;/h2>
&lt;p>Open your Langfuse project. Within a second or two of sending traffic (that&amp;rsquo;s what &lt;code>x-langfuse-ingestion-version: &amp;quot;4&amp;quot;&lt;/code> buys you — real-time previews), traces start landing. Because the gateway emits proper GenAI conventions, Langfuse renders them as full &lt;strong>generations&lt;/strong> with model, token counts, latency, and &lt;strong>cost&lt;/strong> — even for a local Qwen model.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/langfuse-dashboard.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-standalone-langfuse/langfuse-dashboard.png" alt="Langfuse dashboard showing 25 traces tracked and per-user token cost: alice, demo-user-42, bob, carol, dave, totaling $0.12699" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>That &lt;strong>User consumption&lt;/strong> panel is the payoff of the &lt;code>user.id&lt;/code> field in the config — total spend broken out per user (&lt;code>alice&lt;/code> $0.04328, &lt;code>bob&lt;/code> $0.02147, and so on), all from a gateway that the LLM apps never had to be aware of.&lt;/p>
&lt;hr>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>Traces never appear.&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Confirm the gateway box can reach Langfuse: &lt;code>curl -I http://172.16.10.112:3000&lt;/code>.&lt;/li>
&lt;li>Check the startup logs show the &lt;em>real&lt;/em> &lt;code>Authorization: Basic cG...&lt;/code> header, not the &lt;code>${...}&lt;/code> placeholder. If it&amp;rsquo;s still the placeholder, you launched the binary directly instead of via &lt;code>run.sh&lt;/code>.&lt;/li>
&lt;li>Make sure &lt;code>otlpProtocol: http&lt;/code> is set — gRPC silently fails against Langfuse.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>&lt;code>Syntax error: token recognition error at: '$'&lt;/code>&lt;/strong> — the literal placeholder reached the binary. Use &lt;code>run.sh&lt;/code> (or &lt;code>envsubst&lt;/code>) so the secret is substituted before launch.&lt;/p>
&lt;p>&lt;strong>Inputs/outputs show as base64 in Langfuse&lt;/strong> — wrap the body fields in &lt;code>string(...)&lt;/code>, as in the &lt;code>fields.add&lt;/code> block above.&lt;/p>
&lt;hr>
&lt;h2 id="when-to-graduate-to-a-collector">When to graduate to a collector&lt;/h2>
&lt;p>Stay standalone for laptops, single VMs, demos, and MVPs — one binary, one config, done. Reach for the &lt;a href="https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/">OTel Collector pattern&lt;/a> when you need gRPC from the gateway, batching and retries under load, fan-out to more than one observability backend, or you simply don&amp;rsquo;t want the Langfuse secret living anywhere near the gateway process.&lt;/p>
&lt;p>Same traces, same GenAI conventions — just a heavier, more flexible delivery path. Start here; grow into that.&lt;/p>
&lt;hr>
&lt;h2 id="get-the-code">Get the code&lt;/h2>
&lt;p>The complete, runnable setup — &lt;code>config.yaml&lt;/code>, &lt;code>run.sh&lt;/code>, and &lt;code>test.sh&lt;/code> — lives in my demos repo:&lt;/p>
&lt;p>&lt;strong>→ &lt;a href="https://github.com/sebbycorp/agentgateway-demos/tree/main/08-standalone-langfuse">github.com/sebbycorp/agentgateway-demos/tree/main/08-standalone-langfuse&lt;/a>&lt;/strong>&lt;/p>
&lt;p>Clone it, drop your keys into &lt;code>.env&lt;/code>, and &lt;code>./run.sh&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/sebbycorp/agentgateway-demos.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> agentgateway-demos/08-standalone-langfuse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded></item><item><title>Langfuse Integration with agentgateway (OTel Collector Pattern) for cost controls, observability</title><link>https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-10-agentgateway-langfuse-integration-with-otel-collector/</guid><description>&lt;p>&lt;strong>agentgateway&lt;/strong> emits rich OpenTelemetry traces for every LLM request, tool call, and policy decision. This guide shows the production-grade way to forward those traces to Langfuse using an OpenTelemetry Collector — including proper &lt;strong>cost tracking&lt;/strong> even when using local models.&lt;/p>
&lt;hr>
&lt;h2 id="why-go-through-an-otel-collector">Why Go Through an OTel Collector?&lt;/h2>
&lt;p>Directly sending from agentgateway to Langfuse causes problems:&lt;/p>
&lt;ul>
&lt;li>agentgateway parses OTLP headers as CEL expressions&lt;/li>
&lt;li>A raw &lt;code>Authorization: Basic xxx&lt;/code> header makes the proxy crash-loop&lt;/li>
&lt;li>You lose easy fan-out to multiple observability backends&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Best practice&lt;/strong>: agentgateway → OTel Collector (no auth) → Langfuse (with Basic auth)&lt;/p>
&lt;p>This is the exact pattern running in production on the k8s-iceman cluster.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/otel-flow.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/otel-flow.svg" alt="agentgateway to Langfuse trace flow" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>This design keeps agentgateway clean while the collector handles authentication and enrichment.&lt;/p>
&lt;hr>
&lt;h2 id="1-deploy-the-opentelemetry-collector">1. Deploy the OpenTelemetry Collector&lt;/h2>
&lt;p>Use the OpenTelemetry Collector Contrib image with Basic Auth extension:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># helm-values/otel-collector/values.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">fullnameOverride&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">repository&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">extraEnvsFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">servicePort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extensions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">basicauth/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">client_auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:LANGFUSE_PUBLIC_KEY}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:LANGFUSE_SECRET_KEY}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocols&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">grpc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:MY_POD_IP}:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:LANGFUSE_BASE_URL}/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authenticator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">basicauth/langfuse&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extensions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">health_check, basicauth/langfuse]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">memory_limiter, batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="2-configure-agentgateway-tracing">2. Configure agentgateway Tracing&lt;/h2>
&lt;p>Create the &lt;code>AgentgatewayParameters&lt;/code> resource:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># manifests/agentgateway-config/langfuse-tracing.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTLP_ENDPOINT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://otel-collector.kagent.svc.cluster.local:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTLP_PROTOCOL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTLP_HEADERS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{}&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Must be empty object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">span.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;agentgateway.request&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.provider&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;flattenRecursive(llm.prompt)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;flattenRecursive(llm.completion.map(c, {&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;, &amp;#34;content&amp;#34;: c}))&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.completion_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.outputTokens&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.prompt_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.inputTokens&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reference it from your agentgateway Helm values:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">gatewayClassParametersRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agentgateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="3-secrets-via-vault--external-secrets">3. Secrets via Vault + External Secrets&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_PUBLIC_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">key: iceman_langfuse, property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_PUBLIC_KEY }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_SECRET_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">key: iceman_langfuse, property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_SECRET_KEY }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_BASE_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">key: iceman_langfuse, property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_BASE_URL }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="4-cost-tracking-for-local-models">4. Cost Tracking for Local Models&lt;/h2>
&lt;p>Even when using local models (Qwen via vLLM), you can still get proper cost tracking in Langfuse.&lt;/p>
&lt;h3 id="step-1-define-model-pricing-in-langfuse">Step 1: Define Model Pricing in Langfuse&lt;/h3>
&lt;p>Go to &lt;strong>Settings → Models → Add model&lt;/strong> and create an entry.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/cost-tracking.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/cost-tracking.svg" alt="Cost tracking configuration" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Because agentgateway already emits &lt;code>gen_ai.usage.prompt_tokens&lt;/code> and &lt;code>gen_ai.usage.completion_tokens&lt;/code>, Langfuse will automatically calculate cost once the model name matches.&lt;/p>
&lt;h3 id="step-2-verify-in-langfuse-ui">Step 2: Verify in Langfuse UI&lt;/h3>
&lt;p>After sending a few requests through agentgateway you should see:&lt;/p>
&lt;ul>
&lt;li>Token usage columns populated&lt;/li>
&lt;li>Cost column showing your configured price (even if $0)&lt;/li>
&lt;li>Full prompt/completion with rich attributes&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="5-what-you-get-in-langfuse">5. What You Get in Langfuse&lt;/h2>
&lt;p>Every request through agentgateway now appears with:&lt;/p>
&lt;ul>
&lt;li>Full prompt and completion&lt;/li>
&lt;li>Token counts (&lt;code>prompt_tokens&lt;/code>, &lt;code>completion_tokens&lt;/code>)&lt;/li>
&lt;li>Model name&lt;/li>
&lt;li>Cost (auto-calculated from your pricing)&lt;/li>
&lt;li>Which gateway policy ran&lt;/li>
&lt;li>MCP tool calls&lt;/li>
&lt;li>Latency breakdown&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>This pattern gives you production-grade observability:&lt;/p>
&lt;ul>
&lt;li>Clean auth separation via OTel Collector&lt;/li>
&lt;li>Automatic cost tracking even for local models&lt;/li>
&lt;li>No changes required in your agents&lt;/li>
&lt;li>Easy to extend to metrics and logs later&lt;/li>
&lt;/ul>
&lt;p>This is the exact setup running on the k8s-iceman cluster.&lt;/p>
&lt;hr>
&lt;p>Would you like a version that also includes &lt;strong>kagent&lt;/strong> traces in the same diagram?&lt;/p></description><content:encoded>&lt;p>&lt;strong>agentgateway&lt;/strong> emits rich OpenTelemetry traces for every LLM request, tool call, and policy decision. This guide shows the production-grade way to forward those traces to Langfuse using an OpenTelemetry Collector — including proper &lt;strong>cost tracking&lt;/strong> even when using local models.&lt;/p>
&lt;hr>
&lt;h2 id="why-go-through-an-otel-collector">Why Go Through an OTel Collector?&lt;/h2>
&lt;p>Directly sending from agentgateway to Langfuse causes problems:&lt;/p>
&lt;ul>
&lt;li>agentgateway parses OTLP headers as CEL expressions&lt;/li>
&lt;li>A raw &lt;code>Authorization: Basic xxx&lt;/code> header makes the proxy crash-loop&lt;/li>
&lt;li>You lose easy fan-out to multiple observability backends&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Best practice&lt;/strong>: agentgateway → OTel Collector (no auth) → Langfuse (with Basic auth)&lt;/p>
&lt;p>This is the exact pattern running in production on the k8s-iceman cluster.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/otel-flow.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/otel-flow.svg" alt="agentgateway to Langfuse trace flow" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>This design keeps agentgateway clean while the collector handles authentication and enrichment.&lt;/p>
&lt;hr>
&lt;h2 id="1-deploy-the-opentelemetry-collector">1. Deploy the OpenTelemetry Collector&lt;/h2>
&lt;p>Use the OpenTelemetry Collector Contrib image with Basic Auth extension:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># helm-values/otel-collector/values.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">fullnameOverride&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">repository&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">extraEnvsFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">servicePort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extensions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">basicauth/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">client_auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">username&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:LANGFUSE_PUBLIC_KEY}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">password&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:LANGFUSE_SECRET_KEY}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocols&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">grpc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:MY_POD_IP}:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${env:LANGFUSE_BASE_URL}/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authenticator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">basicauth/langfuse&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">extensions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">health_check, basicauth/langfuse]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">memory_limiter, batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="2-configure-agentgateway-tracing">2. Configure agentgateway Tracing&lt;/h2>
&lt;p>Create the &lt;code>AgentgatewayParameters&lt;/code> resource:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># manifests/agentgateway-config/langfuse-tracing.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTLP_ENDPOINT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://otel-collector.kagent.svc.cluster.local:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTLP_PROTOCOL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTLP_HEADERS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;{}&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Must be empty object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">span.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;agentgateway.request&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.provider&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;flattenRecursive(llm.prompt)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;flattenRecursive(llm.completion.map(c, {&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;, &amp;#34;content&amp;#34;: c}))&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.completion_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.outputTokens&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.prompt_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.inputTokens&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reference it from your agentgateway Helm values:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">gatewayClassParametersRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">agentgateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="3-secrets-via-vault--external-secrets">3. Secrets via Vault + External Secrets&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_PUBLIC_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">key: iceman_langfuse, property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_PUBLIC_KEY }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_SECRET_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">key: iceman_langfuse, property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_SECRET_KEY }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_BASE_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">key: iceman_langfuse, property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_BASE_URL }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="4-cost-tracking-for-local-models">4. Cost Tracking for Local Models&lt;/h2>
&lt;p>Even when using local models (Qwen via vLLM), you can still get proper cost tracking in Langfuse.&lt;/p>
&lt;h3 id="step-1-define-model-pricing-in-langfuse">Step 1: Define Model Pricing in Langfuse&lt;/h3>
&lt;p>Go to &lt;strong>Settings → Models → Add model&lt;/strong> and create an entry.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/cost-tracking.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-10-agentgateway-langfuse/cost-tracking.svg" alt="Cost tracking configuration" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Because agentgateway already emits &lt;code>gen_ai.usage.prompt_tokens&lt;/code> and &lt;code>gen_ai.usage.completion_tokens&lt;/code>, Langfuse will automatically calculate cost once the model name matches.&lt;/p>
&lt;h3 id="step-2-verify-in-langfuse-ui">Step 2: Verify in Langfuse UI&lt;/h3>
&lt;p>After sending a few requests through agentgateway you should see:&lt;/p>
&lt;ul>
&lt;li>Token usage columns populated&lt;/li>
&lt;li>Cost column showing your configured price (even if $0)&lt;/li>
&lt;li>Full prompt/completion with rich attributes&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="5-what-you-get-in-langfuse">5. What You Get in Langfuse&lt;/h2>
&lt;p>Every request through agentgateway now appears with:&lt;/p>
&lt;ul>
&lt;li>Full prompt and completion&lt;/li>
&lt;li>Token counts (&lt;code>prompt_tokens&lt;/code>, &lt;code>completion_tokens&lt;/code>)&lt;/li>
&lt;li>Model name&lt;/li>
&lt;li>Cost (auto-calculated from your pricing)&lt;/li>
&lt;li>Which gateway policy ran&lt;/li>
&lt;li>MCP tool calls&lt;/li>
&lt;li>Latency breakdown&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>This pattern gives you production-grade observability:&lt;/p>
&lt;ul>
&lt;li>Clean auth separation via OTel Collector&lt;/li>
&lt;li>Automatic cost tracking even for local models&lt;/li>
&lt;li>No changes required in your agents&lt;/li>
&lt;li>Easy to extend to metrics and logs later&lt;/li>
&lt;/ul>
&lt;p>This is the exact setup running on the k8s-iceman cluster.&lt;/p>
&lt;hr>
&lt;p>Would you like a version that also includes &lt;strong>kagent&lt;/strong> traces in the same diagram?&lt;/p></content:encoded></item><item><title>Self-Hosted Langfuse with Docker + kagent Integration</title><link>https://maniak.io/articles/2026-06-10-self-hosted-langfuse-docker-kagent-integration/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-06-10-self-hosted-langfuse-docker-kagent-integration/</guid><description>&lt;p>Running Langfuse as a self-hosted Docker stack gives you full control over your LLM observability data. This guide shows how to deploy it and integrate it with &lt;strong>kagent&lt;/strong> so every agent trace, prompt, completion, and token count flows into Langfuse automatically.&lt;/p>
&lt;hr>
&lt;h2 id="why-self-hosted-langfuse">Why Self-Hosted Langfuse?&lt;/h2>
&lt;ul>
&lt;li>Keep all prompts and completions inside your infrastructure&lt;/li>
&lt;li>No usage limits or data leaving your cluster&lt;/li>
&lt;li>Full control over retention and cost tracking&lt;/li>
&lt;li>Works great with local models (vLLM, Ollama, etc.)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="1-run-langfuse-with-docker">1. Run Langfuse with Docker&lt;/h2>
&lt;p>The fastest way to get Langfuse running is using their official Docker Compose stack.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/langfuse/langfuse.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After a minute, open http://localhost:3000 and create your first project.&lt;/p>
&lt;p>&lt;strong>Important credentials you’ll need later:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>LANGFUSE_PUBLIC_KEY&lt;/code>&lt;/li>
&lt;li>&lt;code>LANGFUSE_SECRET_KEY&lt;/code>&lt;/li>
&lt;li>Base URL (usually &lt;code>http://langfuse:3000&lt;/code> inside the cluster or your external URL)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="2-configure-kagent-to-send-traces-to-langfuse">2. Configure kagent to Send Traces to Langfuse&lt;/h2>
&lt;p>kagent (v0.9.6+) has built-in OpenTelemetry support. The cleanest way to configure it is through Helm values.&lt;/p>
&lt;h3 id="helm-values-kagent">Helm Values (kagent)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">otel&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://your-langfuse-host:3000/api/public/otel/v1/traces&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http/protobuf&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_OTEL_AUTH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_OTEL_AUTH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTEL_EXPORTER_OTLP_TRACES_HEADERS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Authorization=Basic $(LANGFUSE_OTEL_AUTH)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-auth-secret">Create the Auth Secret&lt;/h3>
&lt;p>Create a secret containing the Basic Auth header:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">LANGFUSE_OTEL_AUTH&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;lt;public_key&amp;gt;:&amp;lt;secret_key&amp;gt;&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># base64 encoded later&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or let &lt;strong>External Secrets Operator + Vault&lt;/strong> manage it (recommended for production).&lt;/p>
&lt;hr>
&lt;h2 id="3-verify-traces-are-flowing">3. Verify Traces Are Flowing&lt;/h2>
&lt;p>Once deployed:&lt;/p>
&lt;ol>
&lt;li>Go to your kagent project in Langfuse&lt;/li>
&lt;li>Trigger any agent (via UI, Telegram, Slack, etc.)&lt;/li>
&lt;li>You should see traces appear within seconds showing:
&lt;ul>
&lt;li>Full prompt + completion&lt;/li>
&lt;li>Token usage (&lt;code>prompt_tokens&lt;/code>, &lt;code>completion_tokens&lt;/code>)&lt;/li>
&lt;li>Model name&lt;/li>
&lt;li>Latency&lt;/li>
&lt;li>Cost (if you configured pricing)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="4-cost-tracking-for-local-models">4. Cost Tracking for Local Models&lt;/h2>
&lt;p>Since you’re likely using local models (Qwen via vLLM), set the price to &lt;code>$0&lt;/code> in Langfuse:&lt;/p>
&lt;p>&lt;strong>Settings → Models → Add model&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Model name: &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code> (must match exactly)&lt;/li>
&lt;li>Input price: &lt;code>0&lt;/code>&lt;/li>
&lt;li>Output price: &lt;code>0&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>This keeps the cost column clean while still capturing full usage data.&lt;/p>
&lt;hr>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>You now have:&lt;/p>
&lt;ul>
&lt;li>Langfuse running self-hosted via Docker&lt;/li>
&lt;li>kagent automatically sending every LLM call via OTLP&lt;/li>
&lt;li>Token usage and (optional) cost tracking working&lt;/li>
&lt;/ul>
&lt;p>This setup is the foundation used in production clusters running kagent + agentgateway.&lt;/p>
&lt;p>Next guide: How to extend this pattern to &lt;strong>agentgateway&lt;/strong> using an OpenTelemetry Collector for cleaner auth handling.&lt;/p></description><content:encoded>&lt;p>Running Langfuse as a self-hosted Docker stack gives you full control over your LLM observability data. This guide shows how to deploy it and integrate it with &lt;strong>kagent&lt;/strong> so every agent trace, prompt, completion, and token count flows into Langfuse automatically.&lt;/p>
&lt;hr>
&lt;h2 id="why-self-hosted-langfuse">Why Self-Hosted Langfuse?&lt;/h2>
&lt;ul>
&lt;li>Keep all prompts and completions inside your infrastructure&lt;/li>
&lt;li>No usage limits or data leaving your cluster&lt;/li>
&lt;li>Full control over retention and cost tracking&lt;/li>
&lt;li>Works great with local models (vLLM, Ollama, etc.)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="1-run-langfuse-with-docker">1. Run Langfuse with Docker&lt;/h2>
&lt;p>The fastest way to get Langfuse running is using their official Docker Compose stack.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git clone https://github.com/langfuse/langfuse.git
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker compose up -d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After a minute, open http://localhost:3000 and create your first project.&lt;/p>
&lt;p>&lt;strong>Important credentials you’ll need later:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>LANGFUSE_PUBLIC_KEY&lt;/code>&lt;/li>
&lt;li>&lt;code>LANGFUSE_SECRET_KEY&lt;/code>&lt;/li>
&lt;li>Base URL (usually &lt;code>http://langfuse:3000&lt;/code> inside the cluster or your external URL)&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="2-configure-kagent-to-send-traces-to-langfuse">2. Configure kagent to Send Traces to Langfuse&lt;/h2>
&lt;p>kagent (v0.9.6+) has built-in OpenTelemetry support. The cleanest way to configure it is through Helm values.&lt;/p>
&lt;h3 id="helm-values-kagent">Helm Values (kagent)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">otel&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://your-langfuse-host:3000/api/public/otel/v1/traces&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http/protobuf&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">controller&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_OTEL_AUTH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LANGFUSE_OTEL_AUTH&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">OTEL_EXPORTER_OTLP_TRACES_HEADERS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Authorization=Basic $(LANGFUSE_OTEL_AUTH)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-auth-secret">Create the Auth Secret&lt;/h3>
&lt;p>Create a secret containing the Basic Auth header:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">LANGFUSE_OTEL_AUTH&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;&amp;lt;public_key&amp;gt;:&amp;lt;secret_key&amp;gt;&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># base64 encoded later&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or let &lt;strong>External Secrets Operator + Vault&lt;/strong> manage it (recommended for production).&lt;/p>
&lt;hr>
&lt;h2 id="3-verify-traces-are-flowing">3. Verify Traces Are Flowing&lt;/h2>
&lt;p>Once deployed:&lt;/p>
&lt;ol>
&lt;li>Go to your kagent project in Langfuse&lt;/li>
&lt;li>Trigger any agent (via UI, Telegram, Slack, etc.)&lt;/li>
&lt;li>You should see traces appear within seconds showing:
&lt;ul>
&lt;li>Full prompt + completion&lt;/li>
&lt;li>Token usage (&lt;code>prompt_tokens&lt;/code>, &lt;code>completion_tokens&lt;/code>)&lt;/li>
&lt;li>Model name&lt;/li>
&lt;li>Latency&lt;/li>
&lt;li>Cost (if you configured pricing)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="4-cost-tracking-for-local-models">4. Cost Tracking for Local Models&lt;/h2>
&lt;p>Since you’re likely using local models (Qwen via vLLM), set the price to &lt;code>$0&lt;/code> in Langfuse:&lt;/p>
&lt;p>&lt;strong>Settings → Models → Add model&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Model name: &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code> (must match exactly)&lt;/li>
&lt;li>Input price: &lt;code>0&lt;/code>&lt;/li>
&lt;li>Output price: &lt;code>0&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>This keeps the cost column clean while still capturing full usage data.&lt;/p>
&lt;hr>
&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>You now have:&lt;/p>
&lt;ul>
&lt;li>Langfuse running self-hosted via Docker&lt;/li>
&lt;li>kagent automatically sending every LLM call via OTLP&lt;/li>
&lt;li>Token usage and (optional) cost tracking working&lt;/li>
&lt;/ul>
&lt;p>This setup is the foundation used in production clusters running kagent + agentgateway.&lt;/p>
&lt;p>Next guide: How to extend this pattern to &lt;strong>agentgateway&lt;/strong> using an OpenTelemetry Collector for cleaner auth handling.&lt;/p></content:encoded></item><item><title>Running kagent on a kind Cluster with a Local vLLM + Qwen3 Backend</title><link>https://maniak.io/articles/2026-06-09-kagent-kind-vllm-qwen/</link><pubDate>Tue, 09 Jun 2026 16:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-06-09-kagent-kind-vllm-qwen/</guid><description>&lt;h1 id="running-kagent-on-a-kind-cluster-with-a-local-vllm--qwen3-backend">Running kagent on a kind Cluster with a Local vLLM + Qwen3 Backend&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>Most kagent walkthroughs assume you&amp;rsquo;re pointing at OpenAI or Anthropic. But what if you want to run the &lt;strong>entire stack locally&lt;/strong> — agents, controller, &lt;em>and&lt;/em> the model — with zero tokens leaving your lab?&lt;/p>
&lt;p>That&amp;rsquo;s exactly what this demo does. We&amp;rsquo;ll spin up a throwaway &lt;a href="https://kind.sigs.k8s.io/">kind&lt;/a> cluster, install &lt;a href="https://kagent.dev">kagent&lt;/a> with &lt;strong>all the built-in agents disabled&lt;/strong>, and wire its default provider to a self-hosted &lt;a href="https://github.com/vllm-project/vllm">vLLM&lt;/a> endpoint serving &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>. From there you can build agents from a clean slate — no cloud LLM, no API bill, no data egress.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/lets-go.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/lets-go.gif" alt="Let&amp;amp;rsquo;s go" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="why-this-setup">Why This Setup?&lt;/h2>
&lt;p>A few reasons this combo is worth your time:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fully local inference&lt;/strong> — the model runs on your own GPU (in my case a DGX Spark), so prompts and cluster data never leave the building.&lt;/li>
&lt;li>&lt;strong>Disposable environment&lt;/strong> — kind means the whole cluster is one &lt;code>docker&lt;/code> container. Break it, delete it, recreate it in 30 seconds.&lt;/li>
&lt;li>&lt;strong>Clean slate&lt;/strong> — disabling the bundled agents means kagent comes up as a bare control plane. You decide which agents exist, instead of inheriting eight of them.&lt;/li>
&lt;li>&lt;strong>OpenAI-compatible&lt;/strong> — vLLM exposes the standard &lt;code>/v1&lt;/code> API, so kagent&amp;rsquo;s &lt;code>openAI&lt;/code> provider talks to it with nothing more than a &lt;code>baseUrl&lt;/code> override.&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ kind cluster (kagent-demo) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent control plane │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (controller + UI) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ provider: openAI │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └──────────────┬─────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ OpenAI /v1 API │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────┼────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ (baseUrl override)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ vLLM server │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Qwen3.6-35B-A3B-FP8 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ DGX Spark · :8000/v1 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The cluster runs everything &lt;em>except&lt;/em> the model. The model lives on a separate box (the DGX Spark) and is reached over the network at &lt;code>http://172.16.10.173:8000/v1&lt;/code>. kagent doesn&amp;rsquo;t care that it&amp;rsquo;s not OpenAI — as far as it&amp;rsquo;s concerned, it&amp;rsquo;s just an OpenAI-compatible endpoint.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/">&lt;code>kind&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code>&lt;/li>
&lt;li>&lt;code>helm&lt;/code>&lt;/li>
&lt;li>A reachable vLLM endpoint serving the &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code> model (see the &lt;a href="#bonus-the-vllm-server-on-dgx-spark">DGX Spark config&lt;/a> at the end)&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-the-kind-cluster">Step 1: Create the kind Cluster&lt;/h2>
&lt;p>One command and you&amp;rsquo;ve got a single-node Kubernetes cluster running inside Docker:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name kagent-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-the-kagent-crds">Step 2: Install the kagent CRDs&lt;/h2>
&lt;p>This installs the Custom Resource Definitions kagent depends on, and — thanks to &lt;code>--create-namespace&lt;/code> — creates the &lt;code>kagent&lt;/code> namespace that &lt;strong>every later step relies on&lt;/strong>. Run it first.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install kagent-crds oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent --create-namespace
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-create-the-vllm-api-key-secret">Step 3: Create the vLLM API Key Secret&lt;/h2>
&lt;p>kagent&amp;rsquo;s &lt;code>openAI&lt;/code> provider expects an API key, even when the backend doesn&amp;rsquo;t enforce one. vLLM typically ignores the key, so any non-empty value works:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic kagent-vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">VLLM_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>dummykey
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>The &lt;code>kagent&lt;/code> namespace already exists from Step 2, so this secret lands in the right place. Order matters here.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-4-install-kagent--without-the-default-agents">Step 4: Install kagent — Without the Default Agents&lt;/h2>
&lt;p>This is the interesting part. The kagent chart ships a set of built-in agents (k8s, helm, istio, cilium, kgateway, promql, observability, argo-rollouts). For this demo we &lt;strong>disable all of them&lt;/strong> so we can build our own agents from scratch. We also point the default provider at our vLLM/Qwen endpoint — just adjust &lt;code>config.baseUrl&lt;/code> to match your own server.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install kagent oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set k8s-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set helm-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set istio-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set promql-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set observability-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set argo-rollouts-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set cilium-debug-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set cilium-manager-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set cilium-policy-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set kgateway-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set grafana-mcp.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set querydoc.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.provider&lt;span class="o">=&lt;/span>OpenAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string providers.openAI.model&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKeySecretRef&lt;span class="o">=&lt;/span>kagent-vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKeySecretKey&lt;span class="o">=&lt;/span>VLLM_API_KEY &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string providers.openAI.config.baseUrl&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://172.16.10.173:8000/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things worth calling out:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Flag&lt;/th>
&lt;th>What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>*-agent.enabled=false&lt;/code>&lt;/td>
&lt;td>Disables each bundled agent so you start clean&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>providers.default=openAI&lt;/code>&lt;/td>
&lt;td>Makes the OpenAI-compatible provider the default&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>providers.openAI.config.baseUrl&lt;/code>&lt;/td>
&lt;td>&lt;strong>The key override&lt;/strong> — points kagent at vLLM instead of api.openai.com&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>providers.openAI.apiKeySecretRef&lt;/code>&lt;/td>
&lt;td>References the dummy secret from Step 3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--set-string&lt;/code> on the model&lt;/td>
&lt;td>Forces the model name to stay a string (it has slashes/digits)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="step-5-verify-the-control-plane">Step 5: Verify the Control Plane&lt;/h2>
&lt;p>Check that the control-plane pods come up and that &lt;strong>no default agents were created&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agents -n kagent &lt;span class="c1"># should be empty&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If &lt;code>get agents&lt;/code> returns nothing, the disable flags did their job. 🎉&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/it-works.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/it-works.gif" alt="It works" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="the-kagent-ui">The kagent UI&lt;/h2>
&lt;p>Once the control plane is healthy, open the kagent UI. The &lt;strong>Models&lt;/strong> view confirms the provider wiring — you should see a &lt;code>default-model-config&lt;/code> pointing at OpenAI as the provider, the Qwen model ID, the &lt;code>kagent-vllm&lt;/code> API key secret, and your vLLM &lt;code>baseUrl&lt;/code> under provider parameters:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-models.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-models.png" alt="kagent Models view showing the default-model-config wired to the local vLLM endpoint" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The &lt;strong>Agents&lt;/strong> view starts empty — exactly what we wanted. Here I&amp;rsquo;ve created a single &lt;code>test&lt;/code> agent from scratch, running on the local &lt;code>OpenAI (Qwen/Qwen3.6-35B-A3B-FP8)&lt;/code> model:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-agents.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-agents.png" alt="kagent Agents view showing a custom test agent on the local Qwen model" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>From here the cluster is yours. Create agents, attach MCP tool servers, and every inference call routes to your local Qwen model instead of a cloud API.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done, tear the whole thing down with a single command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name kagent-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No leftover namespaces, no orphaned resources — the entire cluster was a Docker container, and now it&amp;rsquo;s gone.&lt;/p>
&lt;h2 id="bonus-the-vllm-server-on-dgx-spark">Bonus: The vLLM Server on DGX Spark&lt;/h2>
&lt;p>For completeness, here&amp;rsquo;s the exact &lt;code>docker run&lt;/code> I use to serve Qwen3.6 on my DGX Spark. This is the endpoint kagent&amp;rsquo;s &lt;code>baseUrl&lt;/code> points at:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name vllm-qwen36 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --restart always &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --runtime nvidia --gpus all &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --network host --ipc host --shm-size 96g &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v ~/.cache/huggingface:/root/.cache/huggingface &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> vllm/vllm-openai:cu130-nightly &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model Qwen/Qwen3.6-35B-A3B-FP8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --host 0.0.0.0 --port &lt;span class="m">8000&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.85 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">262144&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">16&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-cudagraph-capture-size &lt;span class="m">256&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --reasoning-parser qwen3 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-auto-tool-choice &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --tool-call-parser qwen3_coder
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Highlights that make this work well for agentic workloads:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>--enable-auto-tool-choice&lt;/code> + &lt;code>--tool-call-parser qwen3_coder&lt;/code>&lt;/strong> — lets the model emit proper tool calls, which is essential for kagent agents that invoke MCP tools.&lt;/li>
&lt;li>&lt;strong>&lt;code>--reasoning-parser qwen3&lt;/code>&lt;/strong> — separates the model&amp;rsquo;s reasoning trace from its final answer.&lt;/li>
&lt;li>&lt;strong>&lt;code>--max-model-len 262144&lt;/code>&lt;/strong> — a generous 256K context window, plenty of room for long agent conversations and tool outputs.&lt;/li>
&lt;li>&lt;strong>&lt;code>--gpu-memory-utilization 0.85&lt;/code>&lt;/strong> — leaves a little headroom on the GPU.&lt;/li>
&lt;/ul>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>In under ten minutes you&amp;rsquo;ve got a disposable Kubernetes cluster running kagent with a &lt;strong>fully local LLM backend&lt;/strong> — no cloud provider, no API key that matters, no data leaving your network. Because the model speaks the OpenAI API, kagent didn&amp;rsquo;t need any special integration: just a &lt;code>baseUrl&lt;/code> swap.&lt;/p>
&lt;p>This is my go-to scratchpad for prototyping agents before promoting them to a real cluster. The kind cluster is cheap to create and free to destroy, and the Qwen3.6 model on the DGX Spark is fast enough to make the loop feel interactive.&lt;/p>
&lt;p>Next up: building a real agent on top of this and wiring it to MCP tool servers. Stay tuned.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Want more on kagent? Check out &lt;a href="https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/">Human-in-the-Loop with kagent&lt;/a> and &lt;a href="https://maniak.io/articles/2026-03-15-fortigate-firewall-telegram-kagent-mcp/">Managing FortiGate Firewalls from Telegram with AI, MCP, and kagent&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;h1 id="running-kagent-on-a-kind-cluster-with-a-local-vllm--qwen3-backend">Running kagent on a kind Cluster with a Local vLLM + Qwen3 Backend&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>Most kagent walkthroughs assume you&amp;rsquo;re pointing at OpenAI or Anthropic. But what if you want to run the &lt;strong>entire stack locally&lt;/strong> — agents, controller, &lt;em>and&lt;/em> the model — with zero tokens leaving your lab?&lt;/p>
&lt;p>That&amp;rsquo;s exactly what this demo does. We&amp;rsquo;ll spin up a throwaway &lt;a href="https://kind.sigs.k8s.io/">kind&lt;/a> cluster, install &lt;a href="https://kagent.dev">kagent&lt;/a> with &lt;strong>all the built-in agents disabled&lt;/strong>, and wire its default provider to a self-hosted &lt;a href="https://github.com/vllm-project/vllm">vLLM&lt;/a> endpoint serving &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>. From there you can build agents from a clean slate — no cloud LLM, no API bill, no data egress.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/lets-go.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/lets-go.gif" alt="Let&amp;amp;rsquo;s go" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="why-this-setup">Why This Setup?&lt;/h2>
&lt;p>A few reasons this combo is worth your time:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fully local inference&lt;/strong> — the model runs on your own GPU (in my case a DGX Spark), so prompts and cluster data never leave the building.&lt;/li>
&lt;li>&lt;strong>Disposable environment&lt;/strong> — kind means the whole cluster is one &lt;code>docker&lt;/code> container. Break it, delete it, recreate it in 30 seconds.&lt;/li>
&lt;li>&lt;strong>Clean slate&lt;/strong> — disabling the bundled agents means kagent comes up as a bare control plane. You decide which agents exist, instead of inheriting eight of them.&lt;/li>
&lt;li>&lt;strong>OpenAI-compatible&lt;/strong> — vLLM exposes the standard &lt;code>/v1&lt;/code> API, so kagent&amp;rsquo;s &lt;code>openAI&lt;/code> provider talks to it with nothing more than a &lt;code>baseUrl&lt;/code> override.&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ kind cluster (kagent-demo) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ kagent control plane │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (controller + UI) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ provider: openAI │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └──────────────┬─────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ OpenAI /v1 API │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────┼────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ (baseUrl override)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ vLLM server │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Qwen3.6-35B-A3B-FP8 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ DGX Spark · :8000/v1 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The cluster runs everything &lt;em>except&lt;/em> the model. The model lives on a separate box (the DGX Spark) and is reached over the network at &lt;code>http://172.16.10.173:8000/v1&lt;/code>. kagent doesn&amp;rsquo;t care that it&amp;rsquo;s not OpenAI — as far as it&amp;rsquo;s concerned, it&amp;rsquo;s just an OpenAI-compatible endpoint.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/">&lt;code>kind&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code>&lt;/li>
&lt;li>&lt;code>helm&lt;/code>&lt;/li>
&lt;li>A reachable vLLM endpoint serving the &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code> model (see the &lt;a href="#bonus-the-vllm-server-on-dgx-spark">DGX Spark config&lt;/a> at the end)&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-the-kind-cluster">Step 1: Create the kind Cluster&lt;/h2>
&lt;p>One command and you&amp;rsquo;ve got a single-node Kubernetes cluster running inside Docker:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name kagent-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-the-kagent-crds">Step 2: Install the kagent CRDs&lt;/h2>
&lt;p>This installs the Custom Resource Definitions kagent depends on, and — thanks to &lt;code>--create-namespace&lt;/code> — creates the &lt;code>kagent&lt;/code> namespace that &lt;strong>every later step relies on&lt;/strong>. Run it first.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install kagent-crds oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent --create-namespace
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-create-the-vllm-api-key-secret">Step 3: Create the vLLM API Key Secret&lt;/h2>
&lt;p>kagent&amp;rsquo;s &lt;code>openAI&lt;/code> provider expects an API key, even when the backend doesn&amp;rsquo;t enforce one. vLLM typically ignores the key, so any non-empty value works:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic kagent-vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">VLLM_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>dummykey
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>The &lt;code>kagent&lt;/code> namespace already exists from Step 2, so this secret lands in the right place. Order matters here.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-4-install-kagent--without-the-default-agents">Step 4: Install kagent — Without the Default Agents&lt;/h2>
&lt;p>This is the interesting part. The kagent chart ships a set of built-in agents (k8s, helm, istio, cilium, kgateway, promql, observability, argo-rollouts). For this demo we &lt;strong>disable all of them&lt;/strong> so we can build our own agents from scratch. We also point the default provider at our vLLM/Qwen endpoint — just adjust &lt;code>config.baseUrl&lt;/code> to match your own server.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install kagent oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set k8s-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set helm-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set istio-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set promql-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set observability-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set argo-rollouts-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set cilium-debug-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set cilium-manager-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set cilium-policy-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set kgateway-agent.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set grafana-mcp.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set querydoc.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.provider&lt;span class="o">=&lt;/span>OpenAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string providers.openAI.model&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKeySecretRef&lt;span class="o">=&lt;/span>kagent-vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKeySecretKey&lt;span class="o">=&lt;/span>VLLM_API_KEY &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string providers.openAI.config.baseUrl&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://172.16.10.173:8000/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things worth calling out:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Flag&lt;/th>
&lt;th>What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>*-agent.enabled=false&lt;/code>&lt;/td>
&lt;td>Disables each bundled agent so you start clean&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>providers.default=openAI&lt;/code>&lt;/td>
&lt;td>Makes the OpenAI-compatible provider the default&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>providers.openAI.config.baseUrl&lt;/code>&lt;/td>
&lt;td>&lt;strong>The key override&lt;/strong> — points kagent at vLLM instead of api.openai.com&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>providers.openAI.apiKeySecretRef&lt;/code>&lt;/td>
&lt;td>References the dummy secret from Step 3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>--set-string&lt;/code> on the model&lt;/td>
&lt;td>Forces the model name to stay a string (it has slashes/digits)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="step-5-verify-the-control-plane">Step 5: Verify the Control Plane&lt;/h2>
&lt;p>Check that the control-plane pods come up and that &lt;strong>no default agents were created&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n kagent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agents -n kagent &lt;span class="c1"># should be empty&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If &lt;code>get agents&lt;/code> returns nothing, the disable flags did their job. 🎉&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/it-works.gif" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/it-works.gif" alt="It works" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="the-kagent-ui">The kagent UI&lt;/h2>
&lt;p>Once the control plane is healthy, open the kagent UI. The &lt;strong>Models&lt;/strong> view confirms the provider wiring — you should see a &lt;code>default-model-config&lt;/code> pointing at OpenAI as the provider, the Qwen model ID, the &lt;code>kagent-vllm&lt;/code> API key secret, and your vLLM &lt;code>baseUrl&lt;/code> under provider parameters:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-models.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-models.png" alt="kagent Models view showing the default-model-config wired to the local vLLM endpoint" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>The &lt;strong>Agents&lt;/strong> view starts empty — exactly what we wanted. Here I&amp;rsquo;ve created a single &lt;code>test&lt;/code> agent from scratch, running on the local &lt;code>OpenAI (Qwen/Qwen3.6-35B-A3B-FP8)&lt;/code> model:&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-agents.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-06-09-kagent-kind-vllm-qwen/kagent-agents.png" alt="kagent Agents view showing a custom test agent on the local Qwen model" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>From here the cluster is yours. Create agents, attach MCP tool servers, and every inference call routes to your local Qwen model instead of a cloud API.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done, tear the whole thing down with a single command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name kagent-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No leftover namespaces, no orphaned resources — the entire cluster was a Docker container, and now it&amp;rsquo;s gone.&lt;/p>
&lt;h2 id="bonus-the-vllm-server-on-dgx-spark">Bonus: The vLLM Server on DGX Spark&lt;/h2>
&lt;p>For completeness, here&amp;rsquo;s the exact &lt;code>docker run&lt;/code> I use to serve Qwen3.6 on my DGX Spark. This is the endpoint kagent&amp;rsquo;s &lt;code>baseUrl&lt;/code> points at:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run -d --name vllm-qwen36 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --restart always &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --runtime nvidia --gpus all &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --network host --ipc host --shm-size 96g &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -v ~/.cache/huggingface:/root/.cache/huggingface &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> vllm/vllm-openai:cu130-nightly &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model Qwen/Qwen3.6-35B-A3B-FP8 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --host 0.0.0.0 --port &lt;span class="m">8000&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.85 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-model-len &lt;span class="m">262144&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-num-seqs &lt;span class="m">16&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-cudagraph-capture-size &lt;span class="m">256&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --reasoning-parser qwen3 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-auto-tool-choice &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --tool-call-parser qwen3_coder
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Highlights that make this work well for agentic workloads:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>--enable-auto-tool-choice&lt;/code> + &lt;code>--tool-call-parser qwen3_coder&lt;/code>&lt;/strong> — lets the model emit proper tool calls, which is essential for kagent agents that invoke MCP tools.&lt;/li>
&lt;li>&lt;strong>&lt;code>--reasoning-parser qwen3&lt;/code>&lt;/strong> — separates the model&amp;rsquo;s reasoning trace from its final answer.&lt;/li>
&lt;li>&lt;strong>&lt;code>--max-model-len 262144&lt;/code>&lt;/strong> — a generous 256K context window, plenty of room for long agent conversations and tool outputs.&lt;/li>
&lt;li>&lt;strong>&lt;code>--gpu-memory-utilization 0.85&lt;/code>&lt;/strong> — leaves a little headroom on the GPU.&lt;/li>
&lt;/ul>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>In under ten minutes you&amp;rsquo;ve got a disposable Kubernetes cluster running kagent with a &lt;strong>fully local LLM backend&lt;/strong> — no cloud provider, no API key that matters, no data leaving your network. Because the model speaks the OpenAI API, kagent didn&amp;rsquo;t need any special integration: just a &lt;code>baseUrl&lt;/code> swap.&lt;/p>
&lt;p>This is my go-to scratchpad for prototyping agents before promoting them to a real cluster. The kind cluster is cheap to create and free to destroy, and the Qwen3.6 model on the DGX Spark is fast enough to make the loop feel interactive.&lt;/p>
&lt;p>Next up: building a real agent on top of this and wiring it to MCP tool servers. Stay tuned.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Want more on kagent? Check out &lt;a href="https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/">Human-in-the-Loop with kagent&lt;/a> and &lt;a href="https://maniak.io/articles/2026-03-15-fortigate-firewall-telegram-kagent-mcp/">Managing FortiGate Firewalls from Telegram with AI, MCP, and kagent&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>Proxying All Your LLM Traffic Through agentgateway with Grok Build</title><link>https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-05-20-proxying-all-llm-traffic-through-agentgateway-with-grok-build/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>If you&amp;rsquo;re like me, your AI tooling has probably grown into a sprawling mess of API keys: Claude Code reads &lt;code>.claude/.env&lt;/code>, Cursor uses OpenAI keys, Langflow points at its own Anthropic endpoint, and your local agents each have their own hardcoded tokens scattered across config files. Every new tool means another key to manage, another endpoint to remember, and another blind spot in your observability stack.&lt;/p>
&lt;p>The alternative is elegant: &lt;strong>route every single LLM request through a single gateway&lt;/strong> that handles authentication, observability, rate limiting, and routing — then point all your tools at that one endpoint.&lt;/p>
&lt;p>In this guide, I&amp;rsquo;ll walk you through exactly how I set up this architecture using &lt;strong>Solo.io Enterprise agentgateway&lt;/strong> as the LLM traffic proxy and &lt;strong>Grok Build&lt;/strong> as one of the clients. My production cluster routes traffic to three backends:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenAI&lt;/strong> (&lt;code>gpt-4o&lt;/code>) — cloud API via &lt;code>api.openai.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>xAI Grok&lt;/strong> (&lt;code>grok-4.3&lt;/code>) — cloud API via &lt;code>api.x.ai&lt;/code>&lt;/li>
&lt;li>&lt;strong>DGX Spark&lt;/strong> (&lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>) — local on-prem inference server at &lt;code>172.16.10.173:8000&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>The key insight is that agentgateway speaks the OpenAI API contract. Every backend — regardless of whether it&amp;rsquo;s OpenAI, xAI, Anthropic, or a local Qwen — is exposed as a standard &lt;code>/v1/chat/completions&lt;/code> endpoint. Your tools don&amp;rsquo;t need to know or care what&amp;rsquo;s behind the proxy.&lt;/p>
&lt;h2 id="architecture-overview">Architecture Overview&lt;/h2>
&lt;p>Here&amp;rsquo;s the high-level picture:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────┐ ┌─────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Grok Build │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code│───→│ agentgateway Proxy (Kubernetes) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Cursor │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Langflow │ │ Gateway → HTTPRoute → AgentgatewayBackend │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Your CLI │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────────┘ └────────┬──────────┬──────────────┬──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────▼───┐ ┌────▼─────┐ ┌─────▼──────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenAI │ │ xAI Grok │ │ DGX Spark │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ gpt-4o │ │ grok-4.3 │ │ Qwen 3.6 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────┘ └──────────┘ └────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>All LLM requests flow through the same agentgateway instance — the same IP, same port, same observability pipeline — but different HTTP path prefixes route to different backends:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path Prefix&lt;/th>
&lt;th>Backend&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Location&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/openai&lt;/code>&lt;/td>
&lt;td>OpenAI&lt;/td>
&lt;td>&lt;code>gpt-4o&lt;/code>&lt;/td>
&lt;td>Cloud (api.openai.com)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/grok&lt;/code>&lt;/td>
&lt;td>xAI&lt;/td>
&lt;td>&lt;code>grok-4.3&lt;/code>&lt;/td>
&lt;td>Cloud (api.x.ai)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/spark&lt;/code>&lt;/td>
&lt;td>DGX Spark&lt;/td>
&lt;td>&lt;code>Qwen3.6-35B-A3B-FP8&lt;/code>&lt;/td>
&lt;td>On-prem (172.16.10.173)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="the-problem-with-scattered-llm-configuration">The Problem with Scattered LLM Configuration&lt;/h2>
&lt;p>Before agentgateway, my setup looked like this:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Grok Build&lt;/strong>: &lt;code>base_url: http://172.16.10.173:8000/v1&lt;/code>, model &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>&lt;/li>
&lt;li>&lt;strong>Claude Code&lt;/strong>: &lt;code>ANTHROPIC_API_KEY&lt;/code> in &lt;code>.env&lt;/code>, points at &lt;code>api.anthropic.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>Cursor&lt;/strong>: OpenAI key hardcoded in settings, points at &lt;code>api.openai.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>Custom agents&lt;/strong>: Various scripts with keys in environment variables, &lt;code>.env&lt;/code> files, or inline&lt;/li>
&lt;/ul>
&lt;p>The pain points were obvious:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>API key sprawl&lt;/strong> — every tool had its own key stored somewhere different&lt;/li>
&lt;li>&lt;strong>No unified observability&lt;/strong> — I couldn&amp;rsquo;t see total LLM spend, compare model performance, or debug issues across providers&lt;/li>
&lt;li>&lt;strong>Local model isolation&lt;/strong> — my on-prem Qwen model on DGX Spark was only accessible to tools that could reach &lt;code>172.16.10.173&lt;/code>, with no unified routing&lt;/li>
&lt;li>&lt;strong>Secret management&lt;/strong> — no centralized rotation, no audit trail&lt;/li>
&lt;/ol>
&lt;p>After agentgateway, it&amp;rsquo;s all one URL with path-based routing. Every request goes through the same gateway that logs, traces, and authenticates — regardless of which LLM ultimately serves it.&lt;/p>
&lt;h2 id="the-architecture-three-layers">The Architecture: Three Layers&lt;/h2>
&lt;p>agentgateway uses three Kubernetes resource types to define each backend connection:&lt;/p>
&lt;h3 id="1-agentgatewaybackend--where-is-the-llm">1. AgentgatewayBackend — &amp;ldquo;Where is the LLM?&amp;rdquo;&lt;/h3>
&lt;p>The backend resource tells agentgateway about an LLM provider. Here are all three of mine:&lt;/p>
&lt;p>&lt;strong>OpenAI backend&lt;/strong> — Cloud API with API key auth:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>xAI Grok backend&lt;/strong> — Cloud API with TLS and SNI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4.3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DGX Spark backend&lt;/strong> — Local on-prem Qwen model, no auth needed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Qwen/Qwen3.6-35B-A3B-FP8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">172.16.10.173&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key observations:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>OpenAI provider&lt;/strong> in agentgateway isn&amp;rsquo;t just for OpenAI — it implements the OpenAI-compatible API contract. That&amp;rsquo;s why it works for xAI (which exposes an OpenAI-compatible endpoint) and for the DGX Spark&amp;rsquo;s vLLM server.&lt;/li>
&lt;li>The DGX Spark backend points directly at the on-prem inference server. No cloud egress, no internet required.&lt;/li>
&lt;li>Cloud backends (OpenAI, xAI) use &lt;code>secretRef&lt;/code> for API key auth. The local DGX Spark has no auth — it&amp;rsquo;s on the internal network.&lt;/li>
&lt;/ul>
&lt;h3 id="2-gateway--where-does-the-traffic-enter">2. Gateway — &amp;ldquo;Where does the traffic enter?&amp;rdquo;&lt;/h3>
&lt;p>A Gateway resource defines the proxy listener. I have one main gateway for cloud providers and dedicated gateways for local models (useful when you want separate NodePort allocations or network policies):&lt;/p>
&lt;p>&lt;strong>Main gateway (cloud LLMs)&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Dedicated DGX Spark gateway&lt;/strong> (separate NodePort for the on-prem model):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Both use the &lt;code>enterprise-agentgateway&lt;/code> GatewayClass, which is created automatically by the Enterprise agentgateway Helm chart. On my bare-metal Talos cluster, these are exposed via NodePort:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Gateway&lt;/th>
&lt;th>NodePort&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agentgateway-proxy&lt;/code>&lt;/td>
&lt;td>30160&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dgx-spark-gateway&lt;/code>&lt;/td>
&lt;td>31944&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="3-httproute--which-backend-handles-this-path">3. HTTPRoute — &amp;ldquo;Which backend handles this path?&amp;rdquo;&lt;/h3>
&lt;p>HTTPRoute resources connect the Gateway to the Backend, defining which URL path prefix routes to which LLM:&lt;/p>
&lt;p>&lt;strong>OpenAI route&lt;/strong> (goes to main gateway, path &lt;code>/openai&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>xAI Grok route&lt;/strong> (goes to dedicated gateway, path &lt;code>/grok&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DGX Spark route&lt;/strong> (goes to dedicated gateway, path &lt;code>/spark&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/spark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The complete flow for a request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Client → http://172.16.10.149:30160/openai/v1/chat/completions
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → Gateway &amp;#34;agentgateway-proxy&amp;#34; (port 80, NodePort 30160)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → HTTPRoute matches path &amp;#34;/openai&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → AgentgatewayBackend &amp;#34;openai&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → api.openai.com/v1/chat/completions
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="secret-management-with-vault">Secret Management with Vault&lt;/h2>
&lt;p>Cloud LLM providers require API keys. I use &lt;strong>HashiCorp Vault&lt;/strong> with the &lt;strong>External Secrets Operator&lt;/strong> to keep all keys out of Git:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Vault (KV v2) ESO K8s Secret Backend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway/llm-keys/openai → ExternalSecret → openai-secret → AgentgatewayBackend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway/llm-keys/xai → ExternalSecret → xai-secret → AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Vault stores the actual API keys. ESO syncs them into Kubernetes Secrets. The &lt;code>AgentgatewayBackend&lt;/code> resources reference those Secrets by name — never the key itself. ArgoCD manages all of this declaratively, and no secret ever touches Git.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># ClusterSecretStore — connects ESO to Vault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://vault-vault.vault.svc:8200&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;agentgateway/llm-keys&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SecretList&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;kubernetes&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">role&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;eso-role&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="distributed-tracing">Distributed Tracing&lt;/h2>
&lt;p>Every request through agentgateway emits OpenTelemetry traces to the Solo UI&amp;rsquo;s telemetry collector. This gives you a unified view of all LLM traffic — OpenAI, xAI, local Qwen — in the same dashboard:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frontend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">solo-enterprise-telemetry-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="connecting-grok-build">Connecting Grok Build&lt;/h2>
&lt;p>Now for the exciting part — pointing Grok Build at the gateway. The Grok Build config file lives at &lt;code>~/.grok/config.toml&lt;/code>. Here&amp;rsquo;s mine:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">cli&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">installer&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;internal&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">qwen&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://172.16.10.173:8000/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agw&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://172.16.10.149:31944/spark&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">max_thoughts_width&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">120&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">fork_secondary_model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;grok-build&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">yolo&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">compact_mode&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">permission_mode&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;always-approve&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">theme&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;groknight&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">models&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">default&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>[model.agw]&lt;/code> section defines a model called &lt;code>agw&lt;/code> that points at the DGX Spark gateway URL — &lt;code>http://172.16.10.149:31944/spark&lt;/code>. This is the agentgateway endpoint for my local Qwen model.&lt;/p>
&lt;p>The &lt;code>[models]&lt;/code> section sets &lt;code>agw&lt;/code> as the &lt;strong>default model&lt;/strong>, so every Grok Build conversation goes through agentgateway by default.&lt;/p>
&lt;p>The &lt;code>[model.qwen]&lt;/code> entry is a fallback direct-to-backend configuration. If I need to bypass the gateway for debugging, I can switch to the &lt;code>qwen&lt;/code> model and it points directly at the vLLM server.&lt;/p>
&lt;p>Here&amp;rsquo;s what happens when I type a prompt in Grok Build:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Grok Build → base_url: http://172.16.10.149:31944/spark
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → Gateway &amp;#34;dgx-spark-gateway&amp;#34; (NodePort 31944)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → HTTPRoute matches &amp;#34;/spark&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → AgentgatewayBackend &amp;#34;dgx-spark-llm&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → http://172.16.10.173:8000/v1/chat/completions
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → Qwen3.6-35B-A3B-FP8 (local DGX)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The request never leaves my network. The gateway still logs it, traces it, and enforces policies — but the payload never traverses the public internet.&lt;/p>
&lt;h2 id="connecting-other-tools">Connecting Other Tools&lt;/h2>
&lt;p>The same pattern works for any tool that supports custom API endpoints:&lt;/p>
&lt;p>&lt;strong>Claude Code&lt;/strong> — set the API base to the agentgateway URL for Anthropic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ANTHROPIC_BASE_URL=http://172.16.10.149:30160/anthropic
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Cursor&lt;/strong> — in settings, set the API endpoint to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">http://172.16.10.149:30160/openai
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And set the model to &lt;code>gpt-4o&lt;/code> — agentgateway strips the path prefix and forwards to the OpenAI backend.&lt;/p>
&lt;p>&lt;strong>Langflow / LlamaIndex / custom scripts&lt;/strong> — same thing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">openai&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">openai&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">OpenAI&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;not-needed-proxy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://172.16.10.149:30160/openai/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;gpt-4o&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Hello!&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>api_key&lt;/code> doesn&amp;rsquo;t need to be a real OpenAI key — it&amp;rsquo;s just passed through by the proxy. The real auth happens server-side via the Vault-synced Secret.&lt;/p>
&lt;h2 id="testing-the-endpoints">Testing the Endpoints&lt;/h2>
&lt;p>You can verify each backend works through the gateway:&lt;/p>
&lt;p>&lt;strong>OpenAI&lt;/strong> (main gateway, path &lt;code>/openai&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://172.16.10.149:30160/openai/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;gpt-4o&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Hello!&amp;#34;}]}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>xAI Grok&lt;/strong> (dedicated gateway, path &lt;code>/grok&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://172.16.10.149:31500/grok/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;grok-4.3&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Hello!&amp;#34;}]}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DGX Spark&lt;/strong> (dedicated gateway, path &lt;code>/spark&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://172.16.10.149:31944/spark/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Hello!&amp;#34;}]}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="deploying-with-gitops">Deploying with GitOps&lt;/h2>
&lt;p>All of these manifests live in a Git repository and are deployed via ArgoCD using sync waves:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Wave 1&lt;/strong> — Gateway API CRDs&lt;/li>
&lt;li>&lt;strong>Wave 2&lt;/strong> — agentgateway CRDs (AgentgatewayBackend, EnterpriseAgentgatewayPolicy types)&lt;/li>
&lt;li>&lt;strong>Wave 3&lt;/strong> — Control plane (controller + proxy)&lt;/li>
&lt;li>&lt;strong>Wave 4&lt;/strong> — HashiCorp Vault (secrets store)&lt;/li>
&lt;li>&lt;strong>Wave 5&lt;/strong> — External Secrets Operator (Vault → K8s Secret sync)&lt;/li>
&lt;li>&lt;strong>Wave 6&lt;/strong> — Solo UI (dashboard + telemetry collector)&lt;/li>
&lt;li>&lt;strong>Wave 7&lt;/strong> — Config (gateways, backends, routes, policies)&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argoproj.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Application&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argocd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">syncWave&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ... (points to config/ directory in Git)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When I add a new LLM backend, I create three YAML files (backend, gateway if needed, route), commit them, and ArgoCD deploys everything automatically. No imperative commands, no manual kubectl apply.&lt;/p>
&lt;h2 id="the-benefits">The Benefits&lt;/h2>
&lt;p>After months of running this setup, the benefits are clear:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>One URL, all LLMs&lt;/strong> — Every tool connects to the same gateway. No scattered keys, no config files spread across machines.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Local-first by default&lt;/strong> — My Grok Build conversations go to the local Qwen model by default (free, fast, private). I switch to GPT-4o or Grok when I need the heavier models. The gateway makes the switch seamless.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Unified observability&lt;/strong> — The Solo UI shows traces from all three backends in one place. I can see latency, error rates, and token usage across providers without logging into three different dashboards.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Local models get cloud treatment&lt;/strong> — The DGX Spark runs behind the same Gateway API routing, tracing, and policy system as the cloud providers. It&amp;rsquo;s a first-class citizen, not an afterthought.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Secrets never touch Git&lt;/strong> — Vault + ESO keeps API keys out of the repository. Adding a new provider means storing a key in Vault and applying three YAML files.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Network isolation&lt;/strong> — The on-prem DGX Spark is on an internal network (&lt;code>172.16.10.173&lt;/code>). agentgateway exposes it to the outside world through the Gateway API — no firewall rules needed on the DGX itself.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>agentgateway transforms the LLM landscape from a fragmented mess of per-tool configurations into a unified, observable, centrally-managed infrastructure. Grok Build is just one client — any tool that speaks the OpenAI API contract can connect to the gateway and get access to all your LLMs.&lt;/p>
&lt;p>The three-resource pattern (Backend → Gateway → Route) is simple enough to understand in minutes but powerful enough to handle complex multi-provider, multi-model, multi-cluster setups. And because it&amp;rsquo;s all Kubernetes-native — using Gateway API CRDs managed by ArgoCD — it fits into any GitOps workflow you already have.&lt;/p>
&lt;p>The full configuration for this setup is open source in the &lt;a href="https://github.com/sebastianmaniak/k8s-goose">k8s-goose repository&lt;/a>, which includes the ArgoCD GitOps pipeline, all YAML manifests, and scripts for deploying on any Kubernetes cluster.&lt;/p>
&lt;p>For a step-by-step walkthrough of the agentgateway setup itself, see &lt;a href="https://maniak.io/articles/01-setting-up-enterprise-agentgateway-on-kind-clusters">Setting Up Enterprise agentgateway on Kind Clusters&lt;/a>.&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>If you&amp;rsquo;re like me, your AI tooling has probably grown into a sprawling mess of API keys: Claude Code reads &lt;code>.claude/.env&lt;/code>, Cursor uses OpenAI keys, Langflow points at its own Anthropic endpoint, and your local agents each have their own hardcoded tokens scattered across config files. Every new tool means another key to manage, another endpoint to remember, and another blind spot in your observability stack.&lt;/p>
&lt;p>The alternative is elegant: &lt;strong>route every single LLM request through a single gateway&lt;/strong> that handles authentication, observability, rate limiting, and routing — then point all your tools at that one endpoint.&lt;/p>
&lt;p>In this guide, I&amp;rsquo;ll walk you through exactly how I set up this architecture using &lt;strong>Solo.io Enterprise agentgateway&lt;/strong> as the LLM traffic proxy and &lt;strong>Grok Build&lt;/strong> as one of the clients. My production cluster routes traffic to three backends:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenAI&lt;/strong> (&lt;code>gpt-4o&lt;/code>) — cloud API via &lt;code>api.openai.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>xAI Grok&lt;/strong> (&lt;code>grok-4.3&lt;/code>) — cloud API via &lt;code>api.x.ai&lt;/code>&lt;/li>
&lt;li>&lt;strong>DGX Spark&lt;/strong> (&lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>) — local on-prem inference server at &lt;code>172.16.10.173:8000&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>The key insight is that agentgateway speaks the OpenAI API contract. Every backend — regardless of whether it&amp;rsquo;s OpenAI, xAI, Anthropic, or a local Qwen — is exposed as a standard &lt;code>/v1/chat/completions&lt;/code> endpoint. Your tools don&amp;rsquo;t need to know or care what&amp;rsquo;s behind the proxy.&lt;/p>
&lt;h2 id="architecture-overview">Architecture Overview&lt;/h2>
&lt;p>Here&amp;rsquo;s the high-level picture:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────┐ ┌─────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Grok Build │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code│───→│ agentgateway Proxy (Kubernetes) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Cursor │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Langflow │ │ Gateway → HTTPRoute → AgentgatewayBackend │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Your CLI │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────────┘ └────────┬──────────┬──────────────┬──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────▼───┐ ┌────▼─────┐ ┌─────▼──────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenAI │ │ xAI Grok │ │ DGX Spark │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ gpt-4o │ │ grok-4.3 │ │ Qwen 3.6 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────┘ └──────────┘ └────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>All LLM requests flow through the same agentgateway instance — the same IP, same port, same observability pipeline — but different HTTP path prefixes route to different backends:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path Prefix&lt;/th>
&lt;th>Backend&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Location&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/openai&lt;/code>&lt;/td>
&lt;td>OpenAI&lt;/td>
&lt;td>&lt;code>gpt-4o&lt;/code>&lt;/td>
&lt;td>Cloud (api.openai.com)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/grok&lt;/code>&lt;/td>
&lt;td>xAI&lt;/td>
&lt;td>&lt;code>grok-4.3&lt;/code>&lt;/td>
&lt;td>Cloud (api.x.ai)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/spark&lt;/code>&lt;/td>
&lt;td>DGX Spark&lt;/td>
&lt;td>&lt;code>Qwen3.6-35B-A3B-FP8&lt;/code>&lt;/td>
&lt;td>On-prem (172.16.10.173)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="the-problem-with-scattered-llm-configuration">The Problem with Scattered LLM Configuration&lt;/h2>
&lt;p>Before agentgateway, my setup looked like this:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Grok Build&lt;/strong>: &lt;code>base_url: http://172.16.10.173:8000/v1&lt;/code>, model &lt;code>Qwen/Qwen3.6-35B-A3B-FP8&lt;/code>&lt;/li>
&lt;li>&lt;strong>Claude Code&lt;/strong>: &lt;code>ANTHROPIC_API_KEY&lt;/code> in &lt;code>.env&lt;/code>, points at &lt;code>api.anthropic.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>Cursor&lt;/strong>: OpenAI key hardcoded in settings, points at &lt;code>api.openai.com&lt;/code>&lt;/li>
&lt;li>&lt;strong>Custom agents&lt;/strong>: Various scripts with keys in environment variables, &lt;code>.env&lt;/code> files, or inline&lt;/li>
&lt;/ul>
&lt;p>The pain points were obvious:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>API key sprawl&lt;/strong> — every tool had its own key stored somewhere different&lt;/li>
&lt;li>&lt;strong>No unified observability&lt;/strong> — I couldn&amp;rsquo;t see total LLM spend, compare model performance, or debug issues across providers&lt;/li>
&lt;li>&lt;strong>Local model isolation&lt;/strong> — my on-prem Qwen model on DGX Spark was only accessible to tools that could reach &lt;code>172.16.10.173&lt;/code>, with no unified routing&lt;/li>
&lt;li>&lt;strong>Secret management&lt;/strong> — no centralized rotation, no audit trail&lt;/li>
&lt;/ol>
&lt;p>After agentgateway, it&amp;rsquo;s all one URL with path-based routing. Every request goes through the same gateway that logs, traces, and authenticates — regardless of which LLM ultimately serves it.&lt;/p>
&lt;h2 id="the-architecture-three-layers">The Architecture: Three Layers&lt;/h2>
&lt;p>agentgateway uses three Kubernetes resource types to define each backend connection:&lt;/p>
&lt;h3 id="1-agentgatewaybackend--where-is-the-llm">1. AgentgatewayBackend — &amp;ldquo;Where is the LLM?&amp;rdquo;&lt;/h3>
&lt;p>The backend resource tells agentgateway about an LLM provider. Here are all three of mine:&lt;/p>
&lt;p>&lt;strong>OpenAI backend&lt;/strong> — Cloud API with API key auth:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>xAI Grok backend&lt;/strong> — Cloud API with TLS and SNI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4.3&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DGX Spark backend&lt;/strong> — Local on-prem Qwen model, no auth needed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Qwen/Qwen3.6-35B-A3B-FP8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">172.16.10.173&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key observations:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>OpenAI provider&lt;/strong> in agentgateway isn&amp;rsquo;t just for OpenAI — it implements the OpenAI-compatible API contract. That&amp;rsquo;s why it works for xAI (which exposes an OpenAI-compatible endpoint) and for the DGX Spark&amp;rsquo;s vLLM server.&lt;/li>
&lt;li>The DGX Spark backend points directly at the on-prem inference server. No cloud egress, no internet required.&lt;/li>
&lt;li>Cloud backends (OpenAI, xAI) use &lt;code>secretRef&lt;/code> for API key auth. The local DGX Spark has no auth — it&amp;rsquo;s on the internal network.&lt;/li>
&lt;/ul>
&lt;h3 id="2-gateway--where-does-the-traffic-enter">2. Gateway — &amp;ldquo;Where does the traffic enter?&amp;rdquo;&lt;/h3>
&lt;p>A Gateway resource defines the proxy listener. I have one main gateway for cloud providers and dedicated gateways for local models (useful when you want separate NodePort allocations or network policies):&lt;/p>
&lt;p>&lt;strong>Main gateway (cloud LLMs)&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Dedicated DGX Spark gateway&lt;/strong> (separate NodePort for the on-prem model):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Both use the &lt;code>enterprise-agentgateway&lt;/code> GatewayClass, which is created automatically by the Enterprise agentgateway Helm chart. On my bare-metal Talos cluster, these are exposed via NodePort:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Gateway&lt;/th>
&lt;th>NodePort&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>agentgateway-proxy&lt;/code>&lt;/td>
&lt;td>30160&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dgx-spark-gateway&lt;/code>&lt;/td>
&lt;td>31944&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="3-httproute--which-backend-handles-this-path">3. HTTPRoute — &amp;ldquo;Which backend handles this path?&amp;rdquo;&lt;/h3>
&lt;p>HTTPRoute resources connect the Gateway to the Backend, defining which URL path prefix routes to which LLM:&lt;/p>
&lt;p>&lt;strong>OpenAI route&lt;/strong> (goes to main gateway, path &lt;code>/openai&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>xAI Grok route&lt;/strong> (goes to dedicated gateway, path &lt;code>/grok&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DGX Spark route&lt;/strong> (goes to dedicated gateway, path &lt;code>/spark&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/spark&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The complete flow for a request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Client → http://172.16.10.149:30160/openai/v1/chat/completions
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → Gateway &amp;#34;agentgateway-proxy&amp;#34; (port 80, NodePort 30160)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → HTTPRoute matches path &amp;#34;/openai&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → AgentgatewayBackend &amp;#34;openai&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → api.openai.com/v1/chat/completions
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="secret-management-with-vault">Secret Management with Vault&lt;/h2>
&lt;p>Cloud LLM providers require API keys. I use &lt;strong>HashiCorp Vault&lt;/strong> with the &lt;strong>External Secrets Operator&lt;/strong> to keep all keys out of Git:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Vault (KV v2) ESO K8s Secret Backend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway/llm-keys/openai → ExternalSecret → openai-secret → AgentgatewayBackend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway/llm-keys/xai → ExternalSecret → xai-secret → AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Vault stores the actual API keys. ESO syncs them into Kubernetes Secrets. The &lt;code>AgentgatewayBackend&lt;/code> resources reference those Secrets by name — never the key itself. ArgoCD manages all of this declaratively, and no secret ever touches Git.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># ClusterSecretStore — connects ESO to Vault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">vault&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">server&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://vault-vault.vault.svc:8200&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;agentgateway/llm-keys&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">SecretList&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;kubernetes&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">role&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;eso-role&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="distributed-tracing">Distributed Tracing&lt;/h2>
&lt;p>Every request through agentgateway emits OpenTelemetry traces to the Solo UI&amp;rsquo;s telemetry collector. This gives you a unified view of all LLM traffic — OpenAI, xAI, local Qwen — in the same dashboard:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dgx-spark-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-grok-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">frontend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">solo-enterprise-telemetry-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="connecting-grok-build">Connecting Grok Build&lt;/h2>
&lt;p>Now for the exciting part — pointing Grok Build at the gateway. The Grok Build config file lives at &lt;code>~/.grok/config.toml&lt;/code>. Here&amp;rsquo;s mine:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">cli&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">installer&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;internal&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">qwen&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://172.16.10.173:8000/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agw&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://172.16.10.149:31944/spark&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">max_thoughts_width&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">120&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">fork_secondary_model&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;grok-build&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">yolo&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">compact_mode&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">permission_mode&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;always-approve&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">theme&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;groknight&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">models&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">default&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agw&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>[model.agw]&lt;/code> section defines a model called &lt;code>agw&lt;/code> that points at the DGX Spark gateway URL — &lt;code>http://172.16.10.149:31944/spark&lt;/code>. This is the agentgateway endpoint for my local Qwen model.&lt;/p>
&lt;p>The &lt;code>[models]&lt;/code> section sets &lt;code>agw&lt;/code> as the &lt;strong>default model&lt;/strong>, so every Grok Build conversation goes through agentgateway by default.&lt;/p>
&lt;p>The &lt;code>[model.qwen]&lt;/code> entry is a fallback direct-to-backend configuration. If I need to bypass the gateway for debugging, I can switch to the &lt;code>qwen&lt;/code> model and it points directly at the vLLM server.&lt;/p>
&lt;p>Here&amp;rsquo;s what happens when I type a prompt in Grok Build:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">Grok Build → base_url: http://172.16.10.149:31944/spark
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → Gateway &amp;#34;dgx-spark-gateway&amp;#34; (NodePort 31944)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → HTTPRoute matches &amp;#34;/spark&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → AgentgatewayBackend &amp;#34;dgx-spark-llm&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → http://172.16.10.173:8000/v1/chat/completions
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → Qwen3.6-35B-A3B-FP8 (local DGX)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The request never leaves my network. The gateway still logs it, traces it, and enforces policies — but the payload never traverses the public internet.&lt;/p>
&lt;h2 id="connecting-other-tools">Connecting Other Tools&lt;/h2>
&lt;p>The same pattern works for any tool that supports custom API endpoints:&lt;/p>
&lt;p>&lt;strong>Claude Code&lt;/strong> — set the API base to the agentgateway URL for Anthropic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">ANTHROPIC_BASE_URL=http://172.16.10.149:30160/anthropic
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Cursor&lt;/strong> — in settings, set the API endpoint to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">http://172.16.10.149:30160/openai
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And set the model to &lt;code>gpt-4o&lt;/code> — agentgateway strips the path prefix and forwards to the OpenAI backend.&lt;/p>
&lt;p>&lt;strong>Langflow / LlamaIndex / custom scripts&lt;/strong> — same thing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">openai&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">openai&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">OpenAI&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;not-needed-proxy&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://172.16.10.149:30160/openai/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;gpt-4o&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Hello!&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>api_key&lt;/code> doesn&amp;rsquo;t need to be a real OpenAI key — it&amp;rsquo;s just passed through by the proxy. The real auth happens server-side via the Vault-synced Secret.&lt;/p>
&lt;h2 id="testing-the-endpoints">Testing the Endpoints&lt;/h2>
&lt;p>You can verify each backend works through the gateway:&lt;/p>
&lt;p>&lt;strong>OpenAI&lt;/strong> (main gateway, path &lt;code>/openai&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://172.16.10.149:30160/openai/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;gpt-4o&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Hello!&amp;#34;}]}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>xAI Grok&lt;/strong> (dedicated gateway, path &lt;code>/grok&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://172.16.10.149:31500/grok/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;grok-4.3&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Hello!&amp;#34;}]}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>DGX Spark&lt;/strong> (dedicated gateway, path &lt;code>/spark&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl http://172.16.10.149:31944/spark/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;:&amp;#34;Qwen/Qwen3.6-35B-A3B-FP8&amp;#34;,&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;Hello!&amp;#34;}]}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="deploying-with-gitops">Deploying with GitOps&lt;/h2>
&lt;p>All of these manifests live in a Git repository and are deployed via ArgoCD using sync waves:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Wave 1&lt;/strong> — Gateway API CRDs&lt;/li>
&lt;li>&lt;strong>Wave 2&lt;/strong> — agentgateway CRDs (AgentgatewayBackend, EnterpriseAgentgatewayPolicy types)&lt;/li>
&lt;li>&lt;strong>Wave 3&lt;/strong> — Control plane (controller + proxy)&lt;/li>
&lt;li>&lt;strong>Wave 4&lt;/strong> — HashiCorp Vault (secrets store)&lt;/li>
&lt;li>&lt;strong>Wave 5&lt;/strong> — External Secrets Operator (Vault → K8s Secret sync)&lt;/li>
&lt;li>&lt;strong>Wave 6&lt;/strong> — Solo UI (dashboard + telemetry collector)&lt;/li>
&lt;li>&lt;strong>Wave 7&lt;/strong> — Config (gateways, backends, routes, policies)&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argoproj.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Application&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argocd&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">syncWave&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># ... (points to config/ directory in Git)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When I add a new LLM backend, I create three YAML files (backend, gateway if needed, route), commit them, and ArgoCD deploys everything automatically. No imperative commands, no manual kubectl apply.&lt;/p>
&lt;h2 id="the-benefits">The Benefits&lt;/h2>
&lt;p>After months of running this setup, the benefits are clear:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>One URL, all LLMs&lt;/strong> — Every tool connects to the same gateway. No scattered keys, no config files spread across machines.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Local-first by default&lt;/strong> — My Grok Build conversations go to the local Qwen model by default (free, fast, private). I switch to GPT-4o or Grok when I need the heavier models. The gateway makes the switch seamless.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Unified observability&lt;/strong> — The Solo UI shows traces from all three backends in one place. I can see latency, error rates, and token usage across providers without logging into three different dashboards.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Local models get cloud treatment&lt;/strong> — The DGX Spark runs behind the same Gateway API routing, tracing, and policy system as the cloud providers. It&amp;rsquo;s a first-class citizen, not an afterthought.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Secrets never touch Git&lt;/strong> — Vault + ESO keeps API keys out of the repository. Adding a new provider means storing a key in Vault and applying three YAML files.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Network isolation&lt;/strong> — The on-prem DGX Spark is on an internal network (&lt;code>172.16.10.173&lt;/code>). agentgateway exposes it to the outside world through the Gateway API — no firewall rules needed on the DGX itself.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>agentgateway transforms the LLM landscape from a fragmented mess of per-tool configurations into a unified, observable, centrally-managed infrastructure. Grok Build is just one client — any tool that speaks the OpenAI API contract can connect to the gateway and get access to all your LLMs.&lt;/p>
&lt;p>The three-resource pattern (Backend → Gateway → Route) is simple enough to understand in minutes but powerful enough to handle complex multi-provider, multi-model, multi-cluster setups. And because it&amp;rsquo;s all Kubernetes-native — using Gateway API CRDs managed by ArgoCD — it fits into any GitOps workflow you already have.&lt;/p>
&lt;p>The full configuration for this setup is open source in the &lt;a href="https://github.com/sebastianmaniak/k8s-goose">k8s-goose repository&lt;/a>, which includes the ArgoCD GitOps pipeline, all YAML manifests, and scripts for deploying on any Kubernetes cluster.&lt;/p>
&lt;p>For a step-by-step walkthrough of the agentgateway setup itself, see &lt;a href="https://maniak.io/articles/01-setting-up-enterprise-agentgateway-on-kind-clusters">Setting Up Enterprise agentgateway on Kind Clusters&lt;/a>.&lt;/p></content:encoded></item><item><title>How To: Connect Claude Code &amp; Codex Through agentgateway (Subscription + API Key)</title><link>https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-05-08-claude-codex-passthrough-through-agentgateway/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>You&amp;rsquo;ve got Claude Code running on your laptop. You&amp;rsquo;ve got Codex open in a tab. Both are just AI coding tools, right? Wrong. Behind the scenes, they&amp;rsquo;re making raw API calls — Claude Code directly to Anthropic, Codex directly to OpenAI. No central visibility. No rate limiting. No policy enforcement. And if you&amp;rsquo;re running multiple agents, you&amp;rsquo;re scattering API keys across every machine.&lt;/p>
&lt;p>Enter &lt;strong>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>agentgateway lets you run a local or cluster proxy that sits between your AI tools and the upstream providers. Every request goes through the gateway first, giving you a single point of control.&lt;/p>
&lt;p>In this guide, I&amp;rsquo;ll walk you through &lt;strong>two patterns&lt;/strong> for connecting Claude Code, Codex, and OpenCode to agentgateway:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Subscription passthrough&lt;/strong> — your tool uses an Anthropic/OpenAI org subscription, no API key needed&lt;/li>
&lt;li>&lt;strong>API key routing&lt;/strong> — you pass API keys through the gateway for auth&lt;/li>
&lt;/ol>
&lt;p>Both patterns are simple. The subscription approach is cleaner if your org already handles billing. The API key approach is more flexible for multi-account setups.&lt;/p>
&lt;p>Let&amp;rsquo;s go.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s the full flow — your AI tools talk to localhost, the gateway routes and authenticates, upstream providers never see your machine directly.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-05-08-claude-codex/agentgateway-flow-diagram.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-05-08-claude-codex/agentgateway-flow-diagram.svg" alt="agentgateway flow diagram showing Claude Code, Codex, and OpenCode routing through agentgateway to Anthropic and OpenAI" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Your tools → gateway (auth + routing + security layers) → providers. One proxy, full visibility.&lt;/p>
&lt;hr>
&lt;h2 id="pattern-1-subscription-passthrough-no-api-key">Pattern 1: Subscription Passthrough (No API Key)&lt;/h2>
&lt;p>This is the simplest setup. Your AI tools connect to agentgateway, which routes them to Anthropic or OpenAI using the organization&amp;rsquo;s subscription or embedded credentials. No API keys on your machine.&lt;/p>
&lt;h3 id="the-setup">The Setup&lt;/h3>
&lt;p>You&amp;rsquo;re running agentgateway locally on port &lt;code>4001&lt;/code>. Claude Code and Codex both connect to it. Here&amp;rsquo;s what the config looks like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4001&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages/count_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropicTokenCount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/codex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/chat/completions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">completions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/responses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">responses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/embeddings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostOverride&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chatgpt.com:443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/backend-api/codex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things to call out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Claude route&lt;/strong> (&lt;code>/claude&lt;/code>) — rewrites the path to &lt;code>/&lt;/code> and routes to the Anthropic provider. Maps the standard Anthropic API paths like &lt;code>/v1/messages&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Codex route&lt;/strong> (&lt;code>/codex&lt;/code>) — this one&amp;rsquo;s slightly more complex because Codex talks to OpenAI&amp;rsquo;s chatgpt.com backend, not the regular API. We set &lt;code>hostOverride: chatgpt.com:443&lt;/code> and &lt;code>pathPrefix: /backend-api/codex&lt;/code> to tell the gateway exactly where to forward.&lt;/li>
&lt;/ul>
&lt;h3 id="claude-code">Claude Code&lt;/h3>
&lt;p>Configure Claude Code to point at your local gateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// .claude/settings.local.json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ANTHROPIC_BASE_URL&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4001/claude&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Claude Code will send all requests to your gateway on port 4001, and the gateway routes them to Anthropic using your org subscription. No API key in your environment variables.&lt;/p>
&lt;h3 id="codex">Codex&lt;/h3>
&lt;p>Configure Codex in its config file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># .codex/config.toml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model_provider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agentgateway&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4001/codex/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">requires_openai_auth&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>requires_openai_auth = true&lt;/code> flag is important — it tells Codex to send the credentials through to the proxy, so the gateway can handle the auth header.&lt;/p>
&lt;hr>
&lt;h2 id="pattern-2-api-key-routing">Pattern 2: API Key Routing&lt;/h2>
&lt;p>This pattern is for when you want to manage API keys centrally. You store your Anthropic and OpenAI keys in environment variables, and the gateway injects them into upstream requests.&lt;/p>
&lt;h3 id="the-setup-1">The Setup&lt;/h3>
&lt;p>First, set your keys:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>cat ~/.api-keys/anthropic.txt&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>cat ~/.api-keys/openai.txt&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then run agentgateway with those keys and bind on a different port (we use &lt;code>4060&lt;/code> here so it doesn&amp;rsquo;t collide with the first pattern):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4060&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendAuth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$ANTHROPIC_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages/count_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropicTokenCount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/codex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendAuth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/chat/completions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">completions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/responses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">responses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/embeddings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key difference from Pattern 1 is the &lt;code>backendAuth&lt;/code> block under each route&amp;rsquo;s &lt;code>policies.urlRewrite&lt;/code>. This tells the gateway to inject the API key into every upstream request. Your tools themselves never see the key — they just talk to localhost.&lt;/p>
&lt;h3 id="claude-code-1">Claude Code&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// .claude/settings.local.json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ANTHROPIC_BASE_URL&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4060/claude&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="codex-1">Codex&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># .codex/config.toml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model_provider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agentgateway&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4060/codex/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">env_key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;OPENAI_API_KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In this pattern, Codex uses &lt;code>env_key&lt;/code> instead of &lt;code>requires_openai_auth&lt;/code>. It&amp;rsquo;ll pull the key from the environment variable you set.&lt;/p>
&lt;hr>
&lt;h2 id="optional-security-layers">Optional: Security Layers&lt;/h2>
&lt;p>The patterns above cover the core routing and auth. But agentgateway also supports a full security posture — rate limiting, JWT auth, input/output validation, and more. Here&amp;rsquo;s a practical guide to layering those on top.&lt;/p>
&lt;h3 id="rate-limiting">Rate Limiting&lt;/h3>
&lt;p>Stop a runaway agent from burning through tokens:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Rate limit Claude Code to 100 requests/hour&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rate-limit-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestsPerHour&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">actions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">reject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">errorCode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">429&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can do token-based limits (e.g., max 1M tokens/day) or request-based limits. Both per-route and per-identifier (IP, user, org header).&lt;/p>
&lt;h3 id="jwt-authentication">JWT Authentication&lt;/h3>
&lt;p>If you&amp;rsquo;re exposing agentgateway externally (not just localhost), protect it with JWT:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secure-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://auth.yourcompany.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">actions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onSuccess&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">set&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Authenticated-User&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">\\$(request.auth.claims.email)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This validates the JWT, rejects unauthorized requests, and forwards the user&amp;rsquo;s email as a header for audit logging.&lt;/p>
&lt;h3 id="input-validation">Input Validation&lt;/h3>
&lt;p>Guard against prompt injection and oversized payloads:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">validate-input&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">inputValidation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxRequestBodySize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1MB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Block requests with known injection patterns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">blockPatterns&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;ignore previous instructions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;bypass system prompt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="output-filtering">Output Filtering&lt;/h3>
&lt;p>Prevent sensitive data from leaking back:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">filter-output&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">outputFilter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Mask emails, phone numbers, SSNs in responses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maskPatterns&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">email&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">phone&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ssn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="requestresponse-transformation">Request/Response Transformation&lt;/h3>
&lt;p>Beyond what we showed in the config, you can strip headers, modify bodies, add middleware headers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestHeaderModifier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Gateway-Version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1.2&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Request-ID&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">\\$(request.id)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">responseHeaderModifier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Gateway-Processed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="putting-it-all-together">Putting It All Together&lt;/h3>
&lt;p>Here&amp;rsquo;s what a production-ready route looks like when you layer it on:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent-secure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://auth.yourcompany.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestsPerHour&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">actions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">reject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">errorCode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">429&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">inputValidation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxRequestBodySize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2MB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">transformations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">toJson(json(request.body).filterKeys(k, k != &amp;#34;context_management&amp;#34;))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Authenticated-User&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">\\$(request.auth.claims.email)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Gateway-Source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages/count_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropicTokenCount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One route. JWT auth, rate limiting, body transformation, audit headers, and the Anthropic backend. That&amp;rsquo;s the power of agentgateway — you configure it once and every tool that connects gets the full security treatment.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">Why Bother?&lt;/h2>
&lt;p>You could just point Claude Code and Codex at Anthropic and OpenAI directly. And you absolutely can. But here&amp;rsquo;s what you get by routing through agentgateway:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Centralized auth&lt;/strong> — your API keys live in one place, not scattered across dev machines&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — every request flows through the gateway, so you get traces, metrics, and logs for free&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong> — set per-agent limits so one noisy tool can&amp;rsquo;t drown out the others&lt;/li>
&lt;li>&lt;strong>Policy enforcement&lt;/strong> — block certain model calls, redirect traffic, add middleware&lt;/li>
&lt;li>&lt;strong>One proxy, multiple tools&lt;/strong> — deploy once, every AI tool on your machine connects through it&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="recap">Recap&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pattern&lt;/th>
&lt;th>Auth&lt;/th>
&lt;th>When to use&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Subscription passthrough&lt;/td>
&lt;td>Org subscription&lt;/td>
&lt;td>Your org handles billing, no API key management needed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API key routing&lt;/td>
&lt;td>&lt;code>backendAuth&lt;/code> with env vars&lt;/td>
&lt;td>Multi-account setup, or you want keys managed at the gateway&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Pick the pattern that fits your setup. Both work — the gateway handles the heavy lifting either way.&lt;/p>
&lt;hr>
&lt;p>&lt;em>This is a living guide. If you run into issues or have a setup that works differently, drop a comment on the &lt;a href="https://github.com/agentgateway/agentgateway">agentgateway GitHub repo&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>You&amp;rsquo;ve got Claude Code running on your laptop. You&amp;rsquo;ve got Codex open in a tab. Both are just AI coding tools, right? Wrong. Behind the scenes, they&amp;rsquo;re making raw API calls — Claude Code directly to Anthropic, Codex directly to OpenAI. No central visibility. No rate limiting. No policy enforcement. And if you&amp;rsquo;re running multiple agents, you&amp;rsquo;re scattering API keys across every machine.&lt;/p>
&lt;p>Enter &lt;strong>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/strong>.&lt;/p>
&lt;p>agentgateway lets you run a local or cluster proxy that sits between your AI tools and the upstream providers. Every request goes through the gateway first, giving you a single point of control.&lt;/p>
&lt;p>In this guide, I&amp;rsquo;ll walk you through &lt;strong>two patterns&lt;/strong> for connecting Claude Code, Codex, and OpenCode to agentgateway:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Subscription passthrough&lt;/strong> — your tool uses an Anthropic/OpenAI org subscription, no API key needed&lt;/li>
&lt;li>&lt;strong>API key routing&lt;/strong> — you pass API keys through the gateway for auth&lt;/li>
&lt;/ol>
&lt;p>Both patterns are simple. The subscription approach is cleaner if your org already handles billing. The API key approach is more flexible for multi-account setups.&lt;/p>
&lt;p>Let&amp;rsquo;s go.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s the full flow — your AI tools talk to localhost, the gateway routes and authenticates, upstream providers never see your machine directly.&lt;/p>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-05-08-claude-codex/agentgateway-flow-diagram.svg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-05-08-claude-codex/agentgateway-flow-diagram.svg" alt="agentgateway flow diagram showing Claude Code, Codex, and OpenCode routing through agentgateway to Anthropic and OpenAI" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>Your tools → gateway (auth + routing + security layers) → providers. One proxy, full visibility.&lt;/p>
&lt;hr>
&lt;h2 id="pattern-1-subscription-passthrough-no-api-key">Pattern 1: Subscription Passthrough (No API Key)&lt;/h2>
&lt;p>This is the simplest setup. Your AI tools connect to agentgateway, which routes them to Anthropic or OpenAI using the organization&amp;rsquo;s subscription or embedded credentials. No API keys on your machine.&lt;/p>
&lt;h3 id="the-setup">The Setup&lt;/h3>
&lt;p>You&amp;rsquo;re running agentgateway locally on port &lt;code>4001&lt;/code>. Claude Code and Codex both connect to it. Here&amp;rsquo;s what the config looks like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4001&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages/count_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropicTokenCount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/codex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/chat/completions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">completions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/responses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">responses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/embeddings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hostOverride&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chatgpt.com:443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/backend-api/codex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things to call out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Claude route&lt;/strong> (&lt;code>/claude&lt;/code>) — rewrites the path to &lt;code>/&lt;/code> and routes to the Anthropic provider. Maps the standard Anthropic API paths like &lt;code>/v1/messages&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Codex route&lt;/strong> (&lt;code>/codex&lt;/code>) — this one&amp;rsquo;s slightly more complex because Codex talks to OpenAI&amp;rsquo;s chatgpt.com backend, not the regular API. We set &lt;code>hostOverride: chatgpt.com:443&lt;/code> and &lt;code>pathPrefix: /backend-api/codex&lt;/code> to tell the gateway exactly where to forward.&lt;/li>
&lt;/ul>
&lt;h3 id="claude-code">Claude Code&lt;/h3>
&lt;p>Configure Claude Code to point at your local gateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// .claude/settings.local.json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ANTHROPIC_BASE_URL&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4001/claude&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Claude Code will send all requests to your gateway on port 4001, and the gateway routes them to Anthropic using your org subscription. No API key in your environment variables.&lt;/p>
&lt;h3 id="codex">Codex&lt;/h3>
&lt;p>Configure Codex in its config file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># .codex/config.toml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model_provider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agentgateway&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4001/codex/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">requires_openai_auth&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>requires_openai_auth = true&lt;/code> flag is important — it tells Codex to send the credentials through to the proxy, so the gateway can handle the auth header.&lt;/p>
&lt;hr>
&lt;h2 id="pattern-2-api-key-routing">Pattern 2: API Key Routing&lt;/h2>
&lt;p>This pattern is for when you want to manage API keys centrally. You store your Anthropic and OpenAI keys in environment variables, and the gateway injects them into upstream requests.&lt;/p>
&lt;h3 id="the-setup-1">The Setup&lt;/h3>
&lt;p>First, set your keys:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>cat ~/.api-keys/anthropic.txt&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>cat ~/.api-keys/openai.txt&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then run agentgateway with those keys and bind on a different port (we use &lt;code>4060&lt;/code> here so it doesn&amp;rsquo;t collide with the first pattern):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">binds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4060&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendAuth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$ANTHROPIC_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages/count_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropicTokenCount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/codex&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendAuth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">codex-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openAI&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/chat/completions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">completions&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/responses&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">responses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/embeddings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">embeddings&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key difference from Pattern 1 is the &lt;code>backendAuth&lt;/code> block under each route&amp;rsquo;s &lt;code>policies.urlRewrite&lt;/code>. This tells the gateway to inject the API key into every upstream request. Your tools themselves never see the key — they just talk to localhost.&lt;/p>
&lt;h3 id="claude-code-1">Claude Code&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// .claude/settings.local.json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ANTHROPIC_BASE_URL&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4060/claude&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="codex-1">Codex&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># .codex/config.toml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">model_provider&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">model_providers&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">agentgateway&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">name&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;agentgateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">base_url&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:4060/codex/v1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">env_key&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;OPENAI_API_KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In this pattern, Codex uses &lt;code>env_key&lt;/code> instead of &lt;code>requires_openai_auth&lt;/code>. It&amp;rsquo;ll pull the key from the environment variable you set.&lt;/p>
&lt;hr>
&lt;h2 id="optional-security-layers">Optional: Security Layers&lt;/h2>
&lt;p>The patterns above cover the core routing and auth. But agentgateway also supports a full security posture — rate limiting, JWT auth, input/output validation, and more. Here&amp;rsquo;s a practical guide to layering those on top.&lt;/p>
&lt;h3 id="rate-limiting">Rate Limiting&lt;/h3>
&lt;p>Stop a runaway agent from burning through tokens:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Rate limit Claude Code to 100 requests/hour&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rate-limit-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestsPerHour&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">actions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">reject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">errorCode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">429&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can do token-based limits (e.g., max 1M tokens/day) or request-based limits. Both per-route and per-identifier (IP, user, org header).&lt;/p>
&lt;h3 id="jwt-authentication">JWT Authentication&lt;/h3>
&lt;p>If you&amp;rsquo;re exposing agentgateway externally (not just localhost), protect it with JWT:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secure-claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://auth.yourcompany.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">actions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">onSuccess&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestHeaders&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">set&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Authenticated-User&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">\\$(request.auth.claims.email)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This validates the JWT, rejects unauthorized requests, and forwards the user&amp;rsquo;s email as a header for audit logging.&lt;/p>
&lt;h3 id="input-validation">Input Validation&lt;/h3>
&lt;p>Guard against prompt injection and oversized payloads:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">validate-input&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">inputValidation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxRequestBodySize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1MB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Block requests with known injection patterns&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">blockPatterns&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;ignore previous instructions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;bypass system prompt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="output-filtering">Output Filtering&lt;/h3>
&lt;p>Prevent sensitive data from leaking back:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">filter-output&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">outputFilter&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Mask emails, phone numbers, SSNs in responses&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maskPatterns&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">email&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">phone&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">ssn&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="requestresponse-transformation">Request/Response Transformation&lt;/h3>
&lt;p>Beyond what we showed in the config, you can strip headers, modify bodies, add middleware headers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestHeaderModifier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Gateway-Version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1.2&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Request-ID&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">\\$(request.id)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">responseHeaderModifier&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Gateway-Processed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="putting-it-all-together">Putting It All Together&lt;/h3>
&lt;p>Here&amp;rsquo;s what a production-ready route looks like when you layer it on:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent-secure&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pathPrefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/claude&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">urlRewrite&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prefix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://auth.yourcompany.com&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requestsPerHour&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">actions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">reject&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">errorCode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">429&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">inputValidation&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxRequestBodySize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2MB&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">transformations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">toJson(json(request.body).filterKeys(k, k != &amp;#34;context_management&amp;#34;))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">set&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Authenticated-User&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">\\$(request.auth.claims.email)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">X-Gateway-Source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backends&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">claude-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">messages&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">/v1/messages/count_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropicTokenCount&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">&amp;#39;*&amp;#39;&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">passthrough&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>One route. JWT auth, rate limiting, body transformation, audit headers, and the Anthropic backend. That&amp;rsquo;s the power of agentgateway — you configure it once and every tool that connects gets the full security treatment.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">Why Bother?&lt;/h2>
&lt;p>You could just point Claude Code and Codex at Anthropic and OpenAI directly. And you absolutely can. But here&amp;rsquo;s what you get by routing through agentgateway:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Centralized auth&lt;/strong> — your API keys live in one place, not scattered across dev machines&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — every request flows through the gateway, so you get traces, metrics, and logs for free&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong> — set per-agent limits so one noisy tool can&amp;rsquo;t drown out the others&lt;/li>
&lt;li>&lt;strong>Policy enforcement&lt;/strong> — block certain model calls, redirect traffic, add middleware&lt;/li>
&lt;li>&lt;strong>One proxy, multiple tools&lt;/strong> — deploy once, every AI tool on your machine connects through it&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="recap">Recap&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pattern&lt;/th>
&lt;th>Auth&lt;/th>
&lt;th>When to use&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Subscription passthrough&lt;/td>
&lt;td>Org subscription&lt;/td>
&lt;td>Your org handles billing, no API key management needed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>API key routing&lt;/td>
&lt;td>&lt;code>backendAuth&lt;/code> with env vars&lt;/td>
&lt;td>Multi-account setup, or you want keys managed at the gateway&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Pick the pattern that fits your setup. Both work — the gateway handles the heavy lifting either way.&lt;/p>
&lt;hr>
&lt;p>&lt;em>This is a living guide. If you run into issues or have a setup that works differently, drop a comment on the &lt;a href="https://github.com/agentgateway/agentgateway">agentgateway GitHub repo&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>GitOps for Agents: Deployment and Management</title><link>https://maniak.io/articles/2026-04-02-gitops-for-agents-deployment-and-management/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-04-02-gitops-for-agents-deployment-and-management/</guid><description>&lt;p>&lt;em>Sebastian Maniak — Solo.io&lt;/em>&lt;/p>
&lt;hr>
&lt;p>If your team has been running Kubernetes for any length of time, you&amp;rsquo;ve probably already adopted some form of GitOps — a Git repository as the source of truth, a reconciliation loop that makes your cluster match what&amp;rsquo;s declared, pull requests as the change management workflow. It&amp;rsquo;s become table stakes for production infrastructure.&lt;/p>
&lt;p>So when AI agents enter the picture, the instinct is natural: treat them like any other workload, deploy them the same way. And plenty of teams are doing exactly that — containerizing their agents, writing Helm charts, pointing Argo CD at a repo, and calling it done.&lt;/p>
&lt;p>That works, up to a point. But agents introduce a set of management challenges that go beyond what traditional GitOps was designed for. An agent isn&amp;rsquo;t just a container with some config. It&amp;rsquo;s a system prompt, a model selection, an effort setting, a set of tools, memory backends, and the non-deterministic reasoning of an LLM — all interacting in ways that make the standard &amp;ldquo;code + config&amp;rdquo; model feel incomplete.&lt;/p>
&lt;p>The question isn&amp;rsquo;t whether to use GitOps for agents. Of course you should. The question is whether your current GitOps setup gives you the right abstractions to manage what actually makes agents different. That&amp;rsquo;s where kagent comes in — it gives Kubernetes a native understanding of what an agent is, so your existing GitOps workflows can manage the things that actually matter.&lt;/p>
&lt;h2 id="what-makes-agents-different-from-other-workloads">What Makes Agents Different From Other Workloads&lt;/h2>
&lt;p>When a microservice misbehaves after a deployment, you can usually point at a code change or a config change and say &amp;ldquo;that&amp;rsquo;s the one.&amp;rdquo; The behavior is deterministic. Same input, same output. Fix the code, redeploy, move on.&lt;/p>
&lt;p>Agents have a wider blast radius.&lt;/p>
&lt;p>An agent&amp;rsquo;s behavior emerges from the intersection of several things at once: its system prompt, its model, its effort setting, the tools it has access to, and the non-deterministic reasoning of the LLM itself. Change any one of those and you might get a meaningfully different agent. Swap the underlying model and the same agent with the same prompt starts making different decisions about which tools to call and in what order. Adjust the effort and a previously reliable agent starts producing inconsistent output. Grant access to one new tool and the agent starts reaching for it in situations you didn&amp;rsquo;t design for.&lt;/p>
&lt;p>If you&amp;rsquo;re deploying agents as generic containers with environment variables and config maps, these changes are all happening at the same level of abstraction. A model swap looks the same as bumping a memory limit. A prompt edit is just another config change. There&amp;rsquo;s no structure that tells your GitOps tooling — or your team reviewing the PR — that one of these changes is cosmetic and the other fundamentally alters how the agent behaves.&lt;/p>
&lt;p>That&amp;rsquo;s the gap. Not &amp;ldquo;do you have GitOps?&amp;rdquo; — you probably do. It&amp;rsquo;s &amp;ldquo;does your GitOps pipeline understand the anatomy of an agent well enough to give you meaningful control?&amp;rdquo;&lt;/p>
&lt;h2 id="what-agent-aware-gitops-actually-looks-like">What Agent-Aware GitOps Actually Looks Like&lt;/h2>
&lt;p>The GitOps principle doesn&amp;rsquo;t change: desired state in Git, reconciliation loop making reality match. What changes is what counts as &amp;ldquo;desired state.&amp;rdquo; When agent-specific concerns become first-class objects in your pipeline — not opaque blobs inside a container — you get three things you didn&amp;rsquo;t have before:&lt;/p>
&lt;p>&lt;strong>Prompts you can actually review.&lt;/strong> Right now, a prompt probably lives somewhere inside your application code, tangled up with everything else. Pull it out into its own declarative resource and suddenly a prompt edit is a focused PR — one diff, one review, one approval. The behavioral change stands on its own instead of hiding inside a code commit.&lt;/p>
&lt;p>&lt;strong>Model upgrades you can roll back in isolation.&lt;/strong> When the model config is its own resource, swapping from one model to another is a single, isolated change. Promote it through dev → staging → production on its own timeline. If quality drops, revert that one resource. Nothing else moves.&lt;/p>
&lt;p>&lt;strong>Tool access you can govern like a permission.&lt;/strong> Each agent declares exactly which tools it can use — not &amp;ldquo;everything this server offers,&amp;rdquo; but a specific, named list. Adding a tool is a one-line diff. Removing one is equally clear. The PR is the approval process for what an agent is allowed to do.&lt;/p>
&lt;h2 id="how-kagent-makes-this-work">How kagent Makes This Work&lt;/h2>
&lt;p>&lt;a href="https://kagent.dev">kagent&lt;/a> is a Kubernetes-native framework that models agents as custom resources — the same declarative, reconcilable building blocks that platform teams already use for everything else. This is the design choice that makes GitOps for agents practical rather than theoretical.&lt;/p>
&lt;p>The architecture splits an agent&amp;rsquo;s definition into independent, composable resources:&lt;/p>
&lt;div class="mermaid">graph TD
 A[Agent] --&amp;gt;|references| MC[Model Config]
 A --&amp;gt;|references| TS1[Tool Server: K8s Tools]
 A --&amp;gt;|references| TS2[Tool Server: Company API]
 A --&amp;gt;|references| M[Memory]
 A --&amp;gt;|advertises skills via| A2A[A2A Protocol]
 A --&amp;gt;|can delegate to| A2[Other Agents]

 MC --&amp;gt;|pulls key from| S[K8s Secret]
 TS1 --&amp;gt;|built &amp;amp; deployed via| KMCP[kmcp]
 TS2 --&amp;gt;|built &amp;amp; deployed via| KMCP
&lt;/div>
&lt;p>The &lt;strong>Agent&lt;/strong> is the top-level resource. It defines the system prompt, references a model configuration, declares which tools are available (and critically, &lt;em>which specific tools&lt;/em> from each tool server — not blanket access), sets resource limits, and advertises skills to other agents via the A2A protocol.&lt;/p>
&lt;p>The &lt;strong>Model Configuration&lt;/strong> is a separate resource that defines the LLM provider, model version, and parameters like temperature and token limits. Because it&amp;rsquo;s independent, you can swap models without touching agent definitions. You can share one model config across twenty agents. You can run a canary by creating a new model config alongside the existing one and pointing one agent at it.&lt;/p>
&lt;p>&lt;strong>Tool Servers&lt;/strong> define the MCP servers that provide capabilities to agents. This is where kagent&amp;rsquo;s skill model lives. Each tool server exposes a set of tools, and agents cherry-pick the specific ones they need. You don&amp;rsquo;t give an agent access to &amp;ldquo;all Kubernetes tools&amp;rdquo; — you give it access to the five read-only tools it actually needs and leave the destructive ones off the list. That scoping decision is declared in the agent resource and reviewed in the PR.&lt;/p>
&lt;p>&lt;strong>Skills&lt;/strong> deserve a closer look. In kagent&amp;rsquo;s model, an agent&amp;rsquo;s capabilities come from tool servers — MCP servers that expose tools the agent can call. &lt;a href="https://github.com/kagent-dev/kmcp">kmcp&lt;/a> is the toolkit for building, testing, and deploying these as production services. kagent ships with built-in tool servers for common operational needs — Kubernetes, Helm, Argo, Istio, Prometheus, Grafana, Cilium — but the real power is in custom skills: your company&amp;rsquo;s internal APIs, your domain-specific workflows, your proprietary data sources, all packaged, versioned, and deployed the same way.&lt;/p>
&lt;p>When an agent declares skills in its A2A configuration, it advertises capabilities that other agents can discover. A supervisor agent doesn&amp;rsquo;t need to know implementation details — it just needs to know the skill exists and how to invoke the agent that has it. Multi-agent orchestration becomes a graph of declared relationships, all visible in the same configuration files.&lt;/p>
&lt;h2 id="the-gitops-workflow-for-agents">The GitOps Workflow for Agents&lt;/h2>
&lt;p>Here&amp;rsquo;s how it all comes together in practice. The flow follows the same base-and-overlay pattern that platform teams already use for microservices, extended to cover the full agent stack:&lt;/p>
&lt;div class="mermaid">graph LR
 subgraph Git Repository
 B[Base Definitions] --&amp;gt; OD[Dev Overlay]
 B --&amp;gt; OS[Staging Overlay]
 B --&amp;gt; OP[Prod Overlay]
 end

 subgraph Change Flow
 DEV[Developer] --&amp;gt;|PR: update prompt| B
 SEC[Security] --&amp;gt;|Review: tool change| B
 ML[ML Engineer] --&amp;gt;|PR: model upgrade| B
 end

 subgraph Argo CD Reconciliation
 OD --&amp;gt;|sync| CD[Dev Cluster]
 OS --&amp;gt;|sync| CS[Staging Cluster]
 OP --&amp;gt;|sync| CP[Prod Cluster]
 end

 CP --&amp;gt;|drift detected| OP
&lt;/div>
&lt;p>The base definitions contain the canonical agent configurations — agent resources, model configs, tool servers, and skills. Overlays customize per environment: dev might use a local model to keep costs down, staging might use a mid-tier cloud model for integration testing, and production runs the most capable model with proper scaling and multiple replicas.&lt;/p>
&lt;p>What makes this powerful for agents specifically is how different types of changes flow through the system:&lt;/p>
&lt;h3 id="prompt-changes-become-code-reviews">Prompt Changes Become Code Reviews&lt;/h3>
&lt;p>When your system prompt lives in a declarative resource in Git, prompt engineering becomes a reviewable activity. Someone rewrites a paragraph of the system prompt — that&amp;rsquo;s a PR with a clear diff. The team can read it, discuss it, push back on it, and approve it. After it&amp;rsquo;s merged, you can trace any line of any system prompt back to the PR that introduced it, the discussion that shaped it, and the person who signed off on it.&lt;/p>
&lt;p>This matters more than it sounds. Prompt changes are the most impactful behavioral changes you can make to an agent. A subtle rewording can completely alter how it uses tools or handles edge cases. Without version control, those changes are invisible. With GitOps, they&amp;rsquo;re first-class, auditable events.&lt;/p>
&lt;h3 id="model-upgrades-become-staged-rollouts">Model Upgrades Become Staged Rollouts&lt;/h3>
&lt;p>New model drops. Instead of flipping a switch across your entire agent fleet, you update the model configuration in your dev overlay. Argo CD syncs it. You run evals. If they pass, you promote the change to staging and run integration tests against real traffic patterns. If staging holds, you promote to production and monitor quality metrics.&lt;/p>
&lt;p>If quality drops at any stage, you revert the model config change — one commit, full rollback, the old model is back in minutes.&lt;/p>
&lt;p>You can even run canaries. Create a new model configuration alongside the existing one, point a single agent at it, and compare quality side-by-side before migrating the rest of the fleet.&lt;/p>
&lt;div class="mermaid">graph LR
 NEW[New Model Config] --&amp;gt;|PR to dev overlay| DEV[Dev]
 DEV --&amp;gt;|evals pass → PR| STG[Staging]
 STG --&amp;gt;|integration tests pass → PR| PROD[Production]
 PROD --&amp;gt;|quality drops| REV[git revert → instant rollback]
&lt;/div>
&lt;h3 id="tool-access-changes-become-security-reviews">Tool Access Changes Become Security Reviews&lt;/h3>
&lt;p>Adding a tool to an agent&amp;rsquo;s allowed list is granting a permission. When an agent gains access to a destructive capability — deleting resources, writing to a database, calling an external API — that&amp;rsquo;s a security-relevant change. GitOps makes it visible by default: the change shows up as a one-line diff in a pull request.&lt;/p>
&lt;p>You can put CODEOWNERS rules on tool configuration so changes require security team review. You can write CI checks that enforce policy — no agent in a customer-facing namespace gets tools that modify infrastructure. The pull request becomes your approval process.&lt;/p>
&lt;h3 id="skill-development-gets-a-real-lifecycle">Skill Development Gets a Real Lifecycle&lt;/h3>
&lt;p>Custom skills — the MCP servers that give agents domain-specific capabilities — live in the same repository and go through the same workflow. A developer builds a new skill with kmcp, tests it locally, commits the source alongside the tool server configuration that deploys it, and opens a PR. The skill goes through review, gets deployed to dev, gets tested, and promotes through environments just like everything else.&lt;/p>
&lt;p>Because tool servers are referenced by name, you can roll out a new version of a skill independently of the agents that use it. Or you can pin specific agents to specific versions if stability matters more than getting the latest capabilities.&lt;/p>
&lt;h2 id="the-reconciliation-loop-why-self-healing-matters-for-agents">The Reconciliation Loop: Why Self-Healing Matters for Agents&lt;/h2>
&lt;p>Any GitOps controller works with kagent — Flux, Argo CD, even a simple apply step in CI. But the reconciliation loop is where the real value lives, and it matters more for agents than for traditional workloads.&lt;/p>
&lt;p>Here&amp;rsquo;s the scenario. It&amp;rsquo;s 2 AM. An incident is happening. Someone &lt;code>kubectl edit&lt;/code>s an agent&amp;rsquo;s system prompt to change its behavior during the incident. Totally reasonable in the moment. The incident resolves, everyone goes to sleep. Three weeks later, the agent is behaving oddly and nobody can figure out why. The team spends half a day investigating before someone thinks to diff the running config against Git and finds the untracked prompt change.&lt;/p>
&lt;p>With GitOps and self-healing enabled, this doesn&amp;rsquo;t happen. The reconciliation loop detects the drift within minutes and reverts the agent to match what&amp;rsquo;s declared in Git. If you actually need to change the agent&amp;rsquo;s behavior, you do it through a PR — which means the change is tracked, reviewed, and reversible.&lt;/p>
&lt;h2 id="what-changes-at-scale">What Changes at Scale&lt;/h2>
&lt;p>The benefits compound as the number of agents grows. With a handful of agents, the overhead of agent-aware GitOps is marginal. At scale — dozens of agents across multiple teams and environments — it starts paying for itself in ways that are hard to get otherwise.&lt;/p>
&lt;p>&lt;strong>Drift detection and self-healing&lt;/strong> mean no agent runs a configuration that wasn&amp;rsquo;t committed and reviewed. &lt;strong>Rollback on any dimension&lt;/strong> — prompt, model, tools — is a git revert away. &lt;strong>Environment promotion&lt;/strong> is a pull request between overlays. &lt;strong>The audit trail&lt;/strong> is just Git history, filterable to any agent&amp;rsquo;s files, with timestamps, authors, and the PR discussions attached.&lt;/p>
&lt;p>&lt;strong>Multi-tenancy&lt;/strong> maps naturally to Kubernetes namespaces. Different teams own different agents, each synced from their own path in the repository. Model configs and tool servers can be shared across namespaces using cross-namespace references, so the platform team provides shared infrastructure while application teams own their agent definitions.&lt;/p>
&lt;p>And when you pair kagent with &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>, the traffic layer — routing, rate limiting, model failover, cost budgets — lives in the same repository and syncs through the same pipeline. One repo describing the full agent stack: what agents do, what skills they have, how their traffic flows, and how it all gets secured.&lt;/p>
&lt;h2 id="where-this-is-heading">Where This Is Heading&lt;/h2>
&lt;p>The teams that are already deploying agents with GitOps are in a good position. The next step is making sure the abstractions match the workload. Treating an agent as a generic container with env vars works until you need to roll back a model change independently of a prompt change, or review a tool access grant as a security decision, or promote a new skill through environments without touching the agents that use it.&lt;/p>
&lt;p>kagent gives Kubernetes a native understanding of these concerns. It fits into the GitOps infrastructure you&amp;rsquo;ve already built — same Argo CD, same Kustomize overlays, same PR workflows — and adds the agent-specific structure that turns generic deployment into meaningful lifecycle management.&lt;/p>
&lt;p>The agents you&amp;rsquo;re running today are probably manageable. The question is whether your setup scales to the agents you&amp;rsquo;ll be running a year from now, when there are more of them, more teams building them, and more at stake when something goes wrong. That&amp;rsquo;s the problem worth solving now.&lt;/p>
&lt;hr>
&lt;p>&lt;em>kagent is a CNCF Sandbox project. &lt;a href="https://kagent.dev">kagent.dev&lt;/a> · &lt;a href="https://github.com/kagent-dev/kagent">github.com/kagent-dev/kagent&lt;/a>&lt;/em>&lt;/p>
&lt;p>&lt;em>agentgateway is an LF / AAIF project. &lt;a href="https://agentgateway.dev">agentgateway.dev&lt;/a>&lt;/em>&lt;/p></description><content:encoded>&lt;p>&lt;em>Sebastian Maniak — Solo.io&lt;/em>&lt;/p>
&lt;hr>
&lt;p>If your team has been running Kubernetes for any length of time, you&amp;rsquo;ve probably already adopted some form of GitOps — a Git repository as the source of truth, a reconciliation loop that makes your cluster match what&amp;rsquo;s declared, pull requests as the change management workflow. It&amp;rsquo;s become table stakes for production infrastructure.&lt;/p>
&lt;p>So when AI agents enter the picture, the instinct is natural: treat them like any other workload, deploy them the same way. And plenty of teams are doing exactly that — containerizing their agents, writing Helm charts, pointing Argo CD at a repo, and calling it done.&lt;/p>
&lt;p>That works, up to a point. But agents introduce a set of management challenges that go beyond what traditional GitOps was designed for. An agent isn&amp;rsquo;t just a container with some config. It&amp;rsquo;s a system prompt, a model selection, an effort setting, a set of tools, memory backends, and the non-deterministic reasoning of an LLM — all interacting in ways that make the standard &amp;ldquo;code + config&amp;rdquo; model feel incomplete.&lt;/p>
&lt;p>The question isn&amp;rsquo;t whether to use GitOps for agents. Of course you should. The question is whether your current GitOps setup gives you the right abstractions to manage what actually makes agents different. That&amp;rsquo;s where kagent comes in — it gives Kubernetes a native understanding of what an agent is, so your existing GitOps workflows can manage the things that actually matter.&lt;/p>
&lt;h2 id="what-makes-agents-different-from-other-workloads">What Makes Agents Different From Other Workloads&lt;/h2>
&lt;p>When a microservice misbehaves after a deployment, you can usually point at a code change or a config change and say &amp;ldquo;that&amp;rsquo;s the one.&amp;rdquo; The behavior is deterministic. Same input, same output. Fix the code, redeploy, move on.&lt;/p>
&lt;p>Agents have a wider blast radius.&lt;/p>
&lt;p>An agent&amp;rsquo;s behavior emerges from the intersection of several things at once: its system prompt, its model, its effort setting, the tools it has access to, and the non-deterministic reasoning of the LLM itself. Change any one of those and you might get a meaningfully different agent. Swap the underlying model and the same agent with the same prompt starts making different decisions about which tools to call and in what order. Adjust the effort and a previously reliable agent starts producing inconsistent output. Grant access to one new tool and the agent starts reaching for it in situations you didn&amp;rsquo;t design for.&lt;/p>
&lt;p>If you&amp;rsquo;re deploying agents as generic containers with environment variables and config maps, these changes are all happening at the same level of abstraction. A model swap looks the same as bumping a memory limit. A prompt edit is just another config change. There&amp;rsquo;s no structure that tells your GitOps tooling — or your team reviewing the PR — that one of these changes is cosmetic and the other fundamentally alters how the agent behaves.&lt;/p>
&lt;p>That&amp;rsquo;s the gap. Not &amp;ldquo;do you have GitOps?&amp;rdquo; — you probably do. It&amp;rsquo;s &amp;ldquo;does your GitOps pipeline understand the anatomy of an agent well enough to give you meaningful control?&amp;rdquo;&lt;/p>
&lt;h2 id="what-agent-aware-gitops-actually-looks-like">What Agent-Aware GitOps Actually Looks Like&lt;/h2>
&lt;p>The GitOps principle doesn&amp;rsquo;t change: desired state in Git, reconciliation loop making reality match. What changes is what counts as &amp;ldquo;desired state.&amp;rdquo; When agent-specific concerns become first-class objects in your pipeline — not opaque blobs inside a container — you get three things you didn&amp;rsquo;t have before:&lt;/p>
&lt;p>&lt;strong>Prompts you can actually review.&lt;/strong> Right now, a prompt probably lives somewhere inside your application code, tangled up with everything else. Pull it out into its own declarative resource and suddenly a prompt edit is a focused PR — one diff, one review, one approval. The behavioral change stands on its own instead of hiding inside a code commit.&lt;/p>
&lt;p>&lt;strong>Model upgrades you can roll back in isolation.&lt;/strong> When the model config is its own resource, swapping from one model to another is a single, isolated change. Promote it through dev → staging → production on its own timeline. If quality drops, revert that one resource. Nothing else moves.&lt;/p>
&lt;p>&lt;strong>Tool access you can govern like a permission.&lt;/strong> Each agent declares exactly which tools it can use — not &amp;ldquo;everything this server offers,&amp;rdquo; but a specific, named list. Adding a tool is a one-line diff. Removing one is equally clear. The PR is the approval process for what an agent is allowed to do.&lt;/p>
&lt;h2 id="how-kagent-makes-this-work">How kagent Makes This Work&lt;/h2>
&lt;p>&lt;a href="https://kagent.dev">kagent&lt;/a> is a Kubernetes-native framework that models agents as custom resources — the same declarative, reconcilable building blocks that platform teams already use for everything else. This is the design choice that makes GitOps for agents practical rather than theoretical.&lt;/p>
&lt;p>The architecture splits an agent&amp;rsquo;s definition into independent, composable resources:&lt;/p>
&lt;div class="mermaid">graph TD
 A[Agent] --&amp;gt;|references| MC[Model Config]
 A --&amp;gt;|references| TS1[Tool Server: K8s Tools]
 A --&amp;gt;|references| TS2[Tool Server: Company API]
 A --&amp;gt;|references| M[Memory]
 A --&amp;gt;|advertises skills via| A2A[A2A Protocol]
 A --&amp;gt;|can delegate to| A2[Other Agents]

 MC --&amp;gt;|pulls key from| S[K8s Secret]
 TS1 --&amp;gt;|built &amp;amp; deployed via| KMCP[kmcp]
 TS2 --&amp;gt;|built &amp;amp; deployed via| KMCP
&lt;/div>
&lt;p>The &lt;strong>Agent&lt;/strong> is the top-level resource. It defines the system prompt, references a model configuration, declares which tools are available (and critically, &lt;em>which specific tools&lt;/em> from each tool server — not blanket access), sets resource limits, and advertises skills to other agents via the A2A protocol.&lt;/p>
&lt;p>The &lt;strong>Model Configuration&lt;/strong> is a separate resource that defines the LLM provider, model version, and parameters like temperature and token limits. Because it&amp;rsquo;s independent, you can swap models without touching agent definitions. You can share one model config across twenty agents. You can run a canary by creating a new model config alongside the existing one and pointing one agent at it.&lt;/p>
&lt;p>&lt;strong>Tool Servers&lt;/strong> define the MCP servers that provide capabilities to agents. This is where kagent&amp;rsquo;s skill model lives. Each tool server exposes a set of tools, and agents cherry-pick the specific ones they need. You don&amp;rsquo;t give an agent access to &amp;ldquo;all Kubernetes tools&amp;rdquo; — you give it access to the five read-only tools it actually needs and leave the destructive ones off the list. That scoping decision is declared in the agent resource and reviewed in the PR.&lt;/p>
&lt;p>&lt;strong>Skills&lt;/strong> deserve a closer look. In kagent&amp;rsquo;s model, an agent&amp;rsquo;s capabilities come from tool servers — MCP servers that expose tools the agent can call. &lt;a href="https://github.com/kagent-dev/kmcp">kmcp&lt;/a> is the toolkit for building, testing, and deploying these as production services. kagent ships with built-in tool servers for common operational needs — Kubernetes, Helm, Argo, Istio, Prometheus, Grafana, Cilium — but the real power is in custom skills: your company&amp;rsquo;s internal APIs, your domain-specific workflows, your proprietary data sources, all packaged, versioned, and deployed the same way.&lt;/p>
&lt;p>When an agent declares skills in its A2A configuration, it advertises capabilities that other agents can discover. A supervisor agent doesn&amp;rsquo;t need to know implementation details — it just needs to know the skill exists and how to invoke the agent that has it. Multi-agent orchestration becomes a graph of declared relationships, all visible in the same configuration files.&lt;/p>
&lt;h2 id="the-gitops-workflow-for-agents">The GitOps Workflow for Agents&lt;/h2>
&lt;p>Here&amp;rsquo;s how it all comes together in practice. The flow follows the same base-and-overlay pattern that platform teams already use for microservices, extended to cover the full agent stack:&lt;/p>
&lt;div class="mermaid">graph LR
 subgraph Git Repository
 B[Base Definitions] --&amp;gt; OD[Dev Overlay]
 B --&amp;gt; OS[Staging Overlay]
 B --&amp;gt; OP[Prod Overlay]
 end

 subgraph Change Flow
 DEV[Developer] --&amp;gt;|PR: update prompt| B
 SEC[Security] --&amp;gt;|Review: tool change| B
 ML[ML Engineer] --&amp;gt;|PR: model upgrade| B
 end

 subgraph Argo CD Reconciliation
 OD --&amp;gt;|sync| CD[Dev Cluster]
 OS --&amp;gt;|sync| CS[Staging Cluster]
 OP --&amp;gt;|sync| CP[Prod Cluster]
 end

 CP --&amp;gt;|drift detected| OP
&lt;/div>
&lt;p>The base definitions contain the canonical agent configurations — agent resources, model configs, tool servers, and skills. Overlays customize per environment: dev might use a local model to keep costs down, staging might use a mid-tier cloud model for integration testing, and production runs the most capable model with proper scaling and multiple replicas.&lt;/p>
&lt;p>What makes this powerful for agents specifically is how different types of changes flow through the system:&lt;/p>
&lt;h3 id="prompt-changes-become-code-reviews">Prompt Changes Become Code Reviews&lt;/h3>
&lt;p>When your system prompt lives in a declarative resource in Git, prompt engineering becomes a reviewable activity. Someone rewrites a paragraph of the system prompt — that&amp;rsquo;s a PR with a clear diff. The team can read it, discuss it, push back on it, and approve it. After it&amp;rsquo;s merged, you can trace any line of any system prompt back to the PR that introduced it, the discussion that shaped it, and the person who signed off on it.&lt;/p>
&lt;p>This matters more than it sounds. Prompt changes are the most impactful behavioral changes you can make to an agent. A subtle rewording can completely alter how it uses tools or handles edge cases. Without version control, those changes are invisible. With GitOps, they&amp;rsquo;re first-class, auditable events.&lt;/p>
&lt;h3 id="model-upgrades-become-staged-rollouts">Model Upgrades Become Staged Rollouts&lt;/h3>
&lt;p>New model drops. Instead of flipping a switch across your entire agent fleet, you update the model configuration in your dev overlay. Argo CD syncs it. You run evals. If they pass, you promote the change to staging and run integration tests against real traffic patterns. If staging holds, you promote to production and monitor quality metrics.&lt;/p>
&lt;p>If quality drops at any stage, you revert the model config change — one commit, full rollback, the old model is back in minutes.&lt;/p>
&lt;p>You can even run canaries. Create a new model configuration alongside the existing one, point a single agent at it, and compare quality side-by-side before migrating the rest of the fleet.&lt;/p>
&lt;div class="mermaid">graph LR
 NEW[New Model Config] --&amp;gt;|PR to dev overlay| DEV[Dev]
 DEV --&amp;gt;|evals pass → PR| STG[Staging]
 STG --&amp;gt;|integration tests pass → PR| PROD[Production]
 PROD --&amp;gt;|quality drops| REV[git revert → instant rollback]
&lt;/div>
&lt;h3 id="tool-access-changes-become-security-reviews">Tool Access Changes Become Security Reviews&lt;/h3>
&lt;p>Adding a tool to an agent&amp;rsquo;s allowed list is granting a permission. When an agent gains access to a destructive capability — deleting resources, writing to a database, calling an external API — that&amp;rsquo;s a security-relevant change. GitOps makes it visible by default: the change shows up as a one-line diff in a pull request.&lt;/p>
&lt;p>You can put CODEOWNERS rules on tool configuration so changes require security team review. You can write CI checks that enforce policy — no agent in a customer-facing namespace gets tools that modify infrastructure. The pull request becomes your approval process.&lt;/p>
&lt;h3 id="skill-development-gets-a-real-lifecycle">Skill Development Gets a Real Lifecycle&lt;/h3>
&lt;p>Custom skills — the MCP servers that give agents domain-specific capabilities — live in the same repository and go through the same workflow. A developer builds a new skill with kmcp, tests it locally, commits the source alongside the tool server configuration that deploys it, and opens a PR. The skill goes through review, gets deployed to dev, gets tested, and promotes through environments just like everything else.&lt;/p>
&lt;p>Because tool servers are referenced by name, you can roll out a new version of a skill independently of the agents that use it. Or you can pin specific agents to specific versions if stability matters more than getting the latest capabilities.&lt;/p>
&lt;h2 id="the-reconciliation-loop-why-self-healing-matters-for-agents">The Reconciliation Loop: Why Self-Healing Matters for Agents&lt;/h2>
&lt;p>Any GitOps controller works with kagent — Flux, Argo CD, even a simple apply step in CI. But the reconciliation loop is where the real value lives, and it matters more for agents than for traditional workloads.&lt;/p>
&lt;p>Here&amp;rsquo;s the scenario. It&amp;rsquo;s 2 AM. An incident is happening. Someone &lt;code>kubectl edit&lt;/code>s an agent&amp;rsquo;s system prompt to change its behavior during the incident. Totally reasonable in the moment. The incident resolves, everyone goes to sleep. Three weeks later, the agent is behaving oddly and nobody can figure out why. The team spends half a day investigating before someone thinks to diff the running config against Git and finds the untracked prompt change.&lt;/p>
&lt;p>With GitOps and self-healing enabled, this doesn&amp;rsquo;t happen. The reconciliation loop detects the drift within minutes and reverts the agent to match what&amp;rsquo;s declared in Git. If you actually need to change the agent&amp;rsquo;s behavior, you do it through a PR — which means the change is tracked, reviewed, and reversible.&lt;/p>
&lt;h2 id="what-changes-at-scale">What Changes at Scale&lt;/h2>
&lt;p>The benefits compound as the number of agents grows. With a handful of agents, the overhead of agent-aware GitOps is marginal. At scale — dozens of agents across multiple teams and environments — it starts paying for itself in ways that are hard to get otherwise.&lt;/p>
&lt;p>&lt;strong>Drift detection and self-healing&lt;/strong> mean no agent runs a configuration that wasn&amp;rsquo;t committed and reviewed. &lt;strong>Rollback on any dimension&lt;/strong> — prompt, model, tools — is a git revert away. &lt;strong>Environment promotion&lt;/strong> is a pull request between overlays. &lt;strong>The audit trail&lt;/strong> is just Git history, filterable to any agent&amp;rsquo;s files, with timestamps, authors, and the PR discussions attached.&lt;/p>
&lt;p>&lt;strong>Multi-tenancy&lt;/strong> maps naturally to Kubernetes namespaces. Different teams own different agents, each synced from their own path in the repository. Model configs and tool servers can be shared across namespaces using cross-namespace references, so the platform team provides shared infrastructure while application teams own their agent definitions.&lt;/p>
&lt;p>And when you pair kagent with &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>, the traffic layer — routing, rate limiting, model failover, cost budgets — lives in the same repository and syncs through the same pipeline. One repo describing the full agent stack: what agents do, what skills they have, how their traffic flows, and how it all gets secured.&lt;/p>
&lt;h2 id="where-this-is-heading">Where This Is Heading&lt;/h2>
&lt;p>The teams that are already deploying agents with GitOps are in a good position. The next step is making sure the abstractions match the workload. Treating an agent as a generic container with env vars works until you need to roll back a model change independently of a prompt change, or review a tool access grant as a security decision, or promote a new skill through environments without touching the agents that use it.&lt;/p>
&lt;p>kagent gives Kubernetes a native understanding of these concerns. It fits into the GitOps infrastructure you&amp;rsquo;ve already built — same Argo CD, same Kustomize overlays, same PR workflows — and adds the agent-specific structure that turns generic deployment into meaningful lifecycle management.&lt;/p>
&lt;p>The agents you&amp;rsquo;re running today are probably manageable. The question is whether your setup scales to the agents you&amp;rsquo;ll be running a year from now, when there are more of them, more teams building them, and more at stake when something goes wrong. That&amp;rsquo;s the problem worth solving now.&lt;/p>
&lt;hr>
&lt;p>&lt;em>kagent is a CNCF Sandbox project. &lt;a href="https://kagent.dev">kagent.dev&lt;/a> · &lt;a href="https://github.com/kagent-dev/kagent">github.com/kagent-dev/kagent&lt;/a>&lt;/em>&lt;/p>
&lt;p>&lt;em>agentgateway is an LF / AAIF project. &lt;a href="https://agentgateway.dev">agentgateway.dev&lt;/a>&lt;/em>&lt;/p></content:encoded></item><item><title>How I Built a Multi-Topic AI Workspace with OpenClaw and Telegram</title><link>https://maniak.io/articles/2026-03-29-openclaw-telegram-topics-ai-workspace/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-03-29-openclaw-telegram-topics-ai-workspace/</guid><description>&lt;h2 id="the-problem-with-one-big-chat">The Problem with One Big Chat&lt;/h2>
&lt;p>When I first set up OpenClaw, I did what most people do — one Telegram chat, one conversation, everything in one place. It worked&amp;hellip; for about a week.&lt;/p>
&lt;p>Then things got messy. I&amp;rsquo;d ask about a Kubernetes issue and the AI would reference a recipe I&amp;rsquo;d asked about earlier. I&amp;rsquo;d try to draft a blog post and it would pull in context from a client billing discussion. The context window — the amount of conversation the AI can hold at once — was polluted with everything from smart home commands to security audits.&lt;/p>
&lt;p>The real problem isn&amp;rsquo;t that the AI is bad at context. It&amp;rsquo;s that &lt;strong>a single conversation thread forces every topic to compete for the same limited context window.&lt;/strong> When that window fills up, older messages get compressed into summaries. Summaries lose details. Your agent slowly forgets who you are, what you&amp;rsquo;re working on, and what you told it yesterday.&lt;/p>
&lt;p>I lost an entire project&amp;rsquo;s context once because of this. Spent 2 hours re-explaining preferences and decisions my agent already knew. Never again.&lt;/p>
&lt;h2 id="the-solution-telegram-forum-topics">The Solution: Telegram Forum Topics&lt;/h2>
&lt;p>Telegram has a feature called &lt;strong>Topics&lt;/strong> (or Forum mode) that turns a group into a structured space where each topic is its own isolated thread. OpenClaw supports this natively — you can configure each topic with:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Its own system prompt&lt;/strong> (personality and instructions)&lt;/li>
&lt;li>&lt;strong>Its own model&lt;/strong> (Opus for deep work, Sonnet for quick tasks)&lt;/li>
&lt;li>&lt;strong>Its own skills&lt;/strong> (only the tools that topic needs)&lt;/li>
&lt;li>&lt;strong>Its own storage&lt;/strong> (Notion page + Google Drive folder)&lt;/li>
&lt;li>&lt;strong>Its own memory path&lt;/strong> (separate daily logs per domain)&lt;/li>
&lt;li>&lt;strong>Its own session&lt;/strong> (isolated context window — zero cross-contamination)&lt;/li>
&lt;/ul>
&lt;p>Think of it as giving your AI a &lt;strong>different desk for each type of work.&lt;/strong> When you walk up to the Content desk, it&amp;rsquo;s already thinking about content. When you walk up to the Infra desk, it&amp;rsquo;s in ops mode. They don&amp;rsquo;t leak into each other.&lt;/p>
&lt;h2 id="my-setup-9-topics">My Setup: 9 Topics&lt;/h2>
&lt;p>Here&amp;rsquo;s how I have mine structured:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Topic&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Skills&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>📋 Mission Control&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Tasks, priorities, reminders, deadlines&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🏠 Home&lt;/td>
&lt;td>Sonnet&lt;/td>
&lt;td>sonoscli, camsnap, weather&lt;/td>
&lt;td>Smart home: Sonos, cameras, devices&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>⚙️ Infra &amp;amp; K8s&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>github, gh-issues, coding-agent, healthcheck&lt;/td>
&lt;td>Kubernetes clusters, logs, incident response&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>✍️ Content&lt;/td>
&lt;td>&lt;strong>Opus&lt;/strong>&lt;/td>
&lt;td>notion, summarize, xurl, blogwatcher&lt;/td>
&lt;td>Scripts, social posts, webinar prep&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🔍 Research&lt;/td>
&lt;td>&lt;strong>Opus&lt;/strong>&lt;/td>
&lt;td>notion, summarize, blogwatcher, github&lt;/td>
&lt;td>Deep dives on people, products, companies&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>💪 Health &amp;amp; Fitness&lt;/td>
&lt;td>Sonnet&lt;/td>
&lt;td>weather&lt;/td>
&lt;td>WHOOP data, workouts, health tracking&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🤖 AI &amp;amp; Agents&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>OpenClaw config, skill development, experiments&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>⚡ Overflow&lt;/td>
&lt;td>Sonnet&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Quick asks that don&amp;rsquo;t fit elsewhere&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>💼 Corp&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Client relationships, billing, time tracking&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="step-by-step-setup">Step-by-Step Setup&lt;/h2>
&lt;h3 id="1-create-the-telegram-forum-group">1. Create the Telegram Forum Group&lt;/h3>
&lt;ol>
&lt;li>Open Telegram → &lt;strong>New Group&lt;/strong> → name it (I called mine &amp;ldquo;OpenClaw&amp;rdquo;)&lt;/li>
&lt;li>Add your OpenClaw bot to the group&lt;/li>
&lt;li>Go to &lt;strong>Group Settings → Edit → Enable Topics&lt;/strong> (Forum mode)&lt;/li>
&lt;li>Create each topic with an emoji prefix for quick scanning&lt;/li>
&lt;/ol>
&lt;p>Each topic gets a numeric ID from Telegram. You&amp;rsquo;ll need these for the config.&lt;/p>
&lt;h3 id="2-get-your-ids">2. Get Your IDs&lt;/h3>
&lt;p>You need three things:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Group chat ID&lt;/strong>: the negative number for your group (check Gateway logs after sending a message)&lt;/li>
&lt;li>&lt;strong>Topic IDs&lt;/strong>: each topic&amp;rsquo;s thread number (also in logs)&lt;/li>
&lt;li>&lt;strong>Your Telegram user ID&lt;/strong>: use &lt;code>@userinfobot&lt;/code> in Telegram&lt;/li>
&lt;/ul>
&lt;h3 id="3-define-your-agents">3. Define Your Agents&lt;/h3>
&lt;p>Before configuring topics, set up agents in &lt;code>openclaw.json&lt;/code>. Each agent maps to a model:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agents: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> defaults: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> model: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> primary: &amp;#34;anthropic/claude-sonnet-4-6&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> fallbacks: [&amp;#34;anthropic/claude-opus-4-6&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> list: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> { id: &amp;#34;main&amp;#34; },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id: &amp;#34;opus&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: &amp;#34;opus&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workspace: &amp;#34;~/.openclaw/workspace&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentDir: &amp;#34;~/.openclaw/agents/opus/agent&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> model: &amp;#34;anthropic/claude-opus-4-6&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id: &amp;#34;sonnet&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: &amp;#34;sonnet&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workspace: &amp;#34;~/.openclaw/workspace&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentDir: &amp;#34;~/.openclaw/agents/sonnet/agent&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> model: &amp;#34;anthropic/claude-sonnet-4-6&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key decisions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>All agents share the same workspace&lt;/strong> — so &lt;code>SOUL.md&lt;/code>, &lt;code>AGENTS.md&lt;/code>, and &lt;code>MEMORY.md&lt;/code> are consistent&lt;/li>
&lt;li>&lt;strong>Each agent gets its own &lt;code>agentDir&lt;/code>&lt;/strong> — for separate auth profiles and sessions&lt;/li>
&lt;li>&lt;strong>Sonnet is the default&lt;/strong> — fast and cheap for 80% of tasks&lt;/li>
&lt;li>&lt;strong>Opus is reserved&lt;/strong> — only for topics that need deep reasoning or creativity&lt;/li>
&lt;/ul>
&lt;h3 id="4-configure-the-telegram-group">4. Configure the Telegram Group&lt;/h3>
&lt;p>Here&amp;rsquo;s the structure inside &lt;code>openclaw.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> telegram: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> enabled: true,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> botToken: &amp;#34;YOUR_BOT_TOKEN&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> dmPolicy: &amp;#34;pairing&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groupPolicy: &amp;#34;allowlist&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groups: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> // Default for ALL groups: require @mention
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;*&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> // Your specific forum group
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;-1003885751519&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowFrom: [&amp;#34;775644809&amp;#34;], // Only YOUR Telegram user ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> topics: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> // Each topic goes here...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="5-configure-each-topic">5. Configure Each Topic&lt;/h3>
&lt;p>Every topic follows the same pattern:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;#34;TOPIC_ID&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentId: &amp;#34;opus&amp;#34;, // or &amp;#34;sonnet&amp;#34;, or omit for default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> skills: [&amp;#34;skill-a&amp;#34;, &amp;#34;skill-b&amp;#34;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> systemPrompt: &amp;#34;MISSION + STYLE + STORAGE + MEMORY&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here&amp;rsquo;s a real example — my Content topic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;#34;48&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentId: &amp;#34;opus&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> skills: [&amp;#34;notion&amp;#34;, &amp;#34;summarize&amp;#34;, &amp;#34;xurl&amp;#34;, &amp;#34;blogwatcher&amp;#34;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> systemPrompt: &amp;#34;You help draft scripts, social posts, and webinar prep. Match the user&amp;#39;s voice and tone. Ask clarifying questions before producing long-form content.\n\nStorage:\n- Notion page: 32d4d457-9015-8138-ba2d-e40df5d4b20e (Content)\n- Google Drive folder: 1__Z8KpROUZucvWbH-4cRzw8tA1BhWFDM (Content)\nUse Notion for drafts, content calendar, and outlines. Use Drive for final assets, slides, and media.\n\nDaily memory: Write dated notes to `memory/content/YYYY-MM-DD.md` for this topic.&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="the-system-prompt-pattern">The System Prompt Pattern&lt;/h3>
&lt;p>Every topic system prompt follows four parts:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>MISSION&lt;/strong> — What this topic does (1-2 sentences)&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;You manage tasks, priorities, and reminders.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>STYLE&lt;/strong> — How to respond&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Be proactive about deadlines. Keep answers practical.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>STORAGE&lt;/strong> — Where to save work&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Notion page: &lt;code>&amp;lt;ID&amp;gt;&lt;/code> (Mission Control) / Drive folder: &lt;code>&amp;lt;ID&amp;gt;&lt;/code>&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>MEMORY&lt;/strong> — Where to write daily logs&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Write dated notes to &lt;code>memory/mission-control/YYYY-MM-DD.md&lt;/code>&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;/ol>
&lt;p>This ensures every topic knows its job, its voice, where to put things, and where to journal. When you open a topic, the AI is already in the right headspace.&lt;/p>
&lt;h2 id="model-routing-the-strategy">Model Routing: The Strategy&lt;/h2>
&lt;p>Not every task needs the most expensive model. Here&amp;rsquo;s my routing logic:&lt;/p>
&lt;p>&lt;strong>Use Opus (frontier model) when:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Creative writing — blog posts, scripts, social content&lt;/li>
&lt;li>Deep research — connecting non-obvious dots across sources&lt;/li>
&lt;li>Complex reasoning — architecture decisions, strategy&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Use Sonnet (fast/cheap) for everything else:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Task management — creating, updating, tracking tasks&lt;/li>
&lt;li>Smart home — &amp;ldquo;play jazz in the kitchen&amp;rdquo; doesn&amp;rsquo;t need genius&lt;/li>
&lt;li>Health data — querying WHOOP stats is structured, not creative&lt;/li>
&lt;li>Quick asks — definitions, lookups, simple questions&lt;/li>
&lt;li>Ops work — kubectl commands, log parsing, incident response&lt;/li>
&lt;/ul>
&lt;p>The result: &lt;strong>~80% of my interactions use Sonnet&lt;/strong> (faster, cheaper), and the 20% that use Opus actually benefit from it.&lt;/p>
&lt;h2 id="skills-routing-keep-it-lean">Skills Routing: Keep It Lean&lt;/h2>
&lt;p>Each topic only loads the skills it needs. This matters because:&lt;/p>
&lt;ul>
&lt;li>Every skill adds instructions to the system prompt&lt;/li>
&lt;li>More instructions = more tokens consumed per turn&lt;/li>
&lt;li>More tokens = faster context window fill-up = more compaction = more forgetting&lt;/li>
&lt;/ul>
&lt;p>My mapping:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Topic&lt;/th>
&lt;th>Skills&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>🏠 Home&lt;/td>
&lt;td>sonoscli, camsnap, weather&lt;/td>
&lt;td>Smart device control&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>⚙️ Infra&lt;/td>
&lt;td>github, gh-issues, coding-agent, healthcheck&lt;/td>
&lt;td>DevOps workflow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>✍️ Content&lt;/td>
&lt;td>notion, summarize, xurl, blogwatcher&lt;/td>
&lt;td>Content pipeline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🔍 Research&lt;/td>
&lt;td>notion, summarize, blogwatcher, github&lt;/td>
&lt;td>Research tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>💪 Health&lt;/td>
&lt;td>weather&lt;/td>
&lt;td>Outdoor planning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Everything else&lt;/td>
&lt;td>&lt;em>(none)&lt;/em>&lt;/td>
&lt;td>Keep it lean&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Rule of thumb:&lt;/strong> if a skill isn&amp;rsquo;t used weekly in a topic, remove it.&lt;/p>
&lt;h2 id="memory-separate-paths-per-domain">Memory: Separate Paths Per Domain&lt;/h2>
&lt;p>This is the key insight that makes the whole architecture work. Each topic writes its daily logs to a &lt;strong>separate memory subdirectory&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">memory/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── mission-control/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── home/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── infra/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── content/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── research/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── health/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── ai/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── corp/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── general/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why this matters:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No cross-contamination&lt;/strong> — infra notes don&amp;rsquo;t pollute your content drafts&lt;/li>
&lt;li>&lt;strong>Vector search stays relevant&lt;/strong> — when searching memory from the Content topic, you find content notes, not Kubernetes debugging logs&lt;/li>
&lt;li>&lt;strong>Compaction is less painful&lt;/strong> — each topic&amp;rsquo;s session is smaller and more focused, so compaction happens less often and loses less context&lt;/li>
&lt;/ul>
&lt;p>Combined with &lt;code>MEMORY.md&lt;/code> for permanent long-term storage (key facts, project overviews, infrastructure details), this creates a layered memory system:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MEMORY.md&lt;/strong> — permanent, cross-topic, loaded every session&lt;/li>
&lt;li>&lt;strong>memory/&lt;domain>/YYYY-MM-DD.md&lt;/strong> — daily, topic-specific, searched on demand&lt;/li>
&lt;/ul>
&lt;h2 id="security-three-layers">Security: Three Layers&lt;/h2>
&lt;p>The config implements three security layers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groupPolicy: &amp;#34;allowlist&amp;#34;, // Layer 1: only approved groups
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groups: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;*&amp;#34;: { requireMention: true }, // Layer 2: strangers must @mention
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;-1003885751519&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowFrom: [&amp;#34;775644809&amp;#34;] // Layer 3: only YOUR user ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> dmPolicy: &amp;#34;pairing&amp;#34; // DMs require pairing code
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Even if someone joins your Telegram group:&lt;/p>
&lt;ul>
&lt;li>They can&amp;rsquo;t trigger the bot in other groups (&lt;code>groupPolicy: &amp;quot;allowlist&amp;quot;&lt;/code>)&lt;/li>
&lt;li>They&amp;rsquo;d need to @mention the bot in random groups (&lt;code>requireMention: true&lt;/code> on &lt;code>&amp;quot;*&amp;quot;&lt;/code>)&lt;/li>
&lt;li>They can&amp;rsquo;t trigger actions in your forum (&lt;code>allowFrom&lt;/code> restricts to your user ID)&lt;/li>
&lt;/ul>
&lt;h2 id="storage-per-topic-notion--drive">Storage: Per-Topic Notion + Drive&lt;/h2>
&lt;p>Each topic has its own Notion page and Google Drive folder. This means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Content topic&lt;/strong> → drafts go to the Content Notion page, final slides go to the Content Drive folder&lt;/li>
&lt;li>&lt;strong>Infra topic&lt;/strong> → runbooks go to the Infra Notion page, log dumps go to the Infra Drive folder&lt;/li>
&lt;li>&lt;strong>Corp topic&lt;/strong> → client notes go to the Corp Notion page, contracts go to the Corp Drive folder&lt;/li>
&lt;/ul>
&lt;p>No hunting. No &amp;ldquo;which folder was that in?&amp;rdquo; Everything is organized by domain from the moment it&amp;rsquo;s created.&lt;/p>
&lt;h2 id="tips-from-3-months-of-use">Tips From 3+ Months of Use&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Start with 3-4 topics&lt;/strong> — add more when you find natural boundaries&lt;/li>
&lt;li>&lt;strong>Overflow is essential&lt;/strong> — random questions need somewhere to go without polluting focused topics&lt;/li>
&lt;li>&lt;strong>Emoji prefixes matter&lt;/strong> — when you have 9 topics, visual scanning speed matters&lt;/li>
&lt;li>&lt;strong>Review monthly&lt;/strong> — merge underused topics, split overloaded ones&lt;/li>
&lt;li>&lt;strong>Pin important messages&lt;/strong> in each topic for quick reference&lt;/li>
&lt;li>&lt;strong>Keep system prompts under 500 words&lt;/strong> — they&amp;rsquo;re injected every turn&lt;/li>
&lt;li>&lt;strong>Memory subdirectories are non-negotiable&lt;/strong> — this is what prevents context bleed&lt;/li>
&lt;li>&lt;strong>Use a Meta topic&lt;/strong> (my &amp;ldquo;AI &amp;amp; Agents&amp;rdquo;) for discussing the setup itself&lt;/li>
&lt;li>&lt;strong>Back up &lt;code>openclaw.json&lt;/code>&lt;/strong> — it&amp;rsquo;s the single source of truth for this entire architecture&lt;/li>
&lt;li>&lt;strong>The cheapest model that works is the right model&lt;/strong> — don&amp;rsquo;t waste Opus on &amp;ldquo;what&amp;rsquo;s the weather&amp;rdquo;&lt;/li>
&lt;/ol>
&lt;h2 id="the-complete-config">The Complete Config&lt;/h2>
&lt;p>Here&amp;rsquo;s the full relevant section of my &lt;code>openclaw.json&lt;/code> for reference:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">telegram&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enabled&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">botToken&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;YOUR_BOT_TOKEN&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dmPolicy&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;pairing&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">groupPolicy&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;allowlist&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">streaming&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;off&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">groups&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">true&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;-YOUR_GROUP_ID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">allowFrom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;YOUR_USER_ID&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">topics&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;45&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You manage tasks, priorities, and reminders. Be proactive about deadlines and follow-ups. Track what&amp;#39;s pending and surface what needs attention.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Mission Control)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Mission Control)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">Use Notion for tasks, SOPs, and structured notes. Use Drive for file attachments and exports.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/mission-control/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;46&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sonoscli&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;camsnap&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;weather&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You assist with home automation: Sonos, cameras, and smart devices. Keep answers practical and action-oriented.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Home)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Home)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/home/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;47&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;gh-issues&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;coding-agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;healthcheck&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You assist with Kubernetes clusters, logs, and incident response. Be concise and ops-focused. Prefer actionable commands over lengthy explanations.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Infra &amp;amp; K8s)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Infra &amp;amp; K8s)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/infra/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;48&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;opus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;notion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;summarize&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;xurl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;blogwatcher&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You help draft scripts, social posts, and webinar prep. Match the user&amp;#39;s voice and tone. Ask clarifying questions before producing long-form content.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Content)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Content)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/content/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;49&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;opus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;notion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;summarize&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;blogwatcher&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You research people, products, companies, and ideas. Cite sources, be thorough, and surface non-obvious connections.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Research)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Research)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/research/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;50&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;weather&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You help with WHOOP data, workout planning, and health dashboard work. Be data-driven and concise.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Health &amp;amp; Fitness)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Health &amp;amp; Fitness)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/health/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;51&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You help with OpenClaw configuration, skill development, and agent experiments. You can be technical and detailed here.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (AI &amp;amp; Agents)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (AI &amp;amp; Agents)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/ai/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;66&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You handle overflow and quick asks. Keep responses short and direct.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/general/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;387&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You manage customer relationships, project billing, and time tracking. Focus on: active clients, logged hours, invoices, and follow-ups. Be direct — flag unbilled time, overdue invoices, and upcoming deadlines.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Corp)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Corp)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/corp/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agents&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">defaults&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;anthropic/claude-sonnet-4-6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fallbacks&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;anthropic/claude-opus-4-6&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">list&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;opus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">workspace&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentDir&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/agents/opus/agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;anthropic/claude-opus-4-6&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">workspace&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentDir&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/agents/sonnet/agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;anthropic/claude-sonnet-4-6&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;p>This setup has transformed how I interact with AI. Instead of one overwhelmed assistant trying to be everything, I have a team of specialists — each focused, each equipped, each with its own memory. The context window problem is essentially solved because no single topic accumulates enough history to trigger aggressive compaction.&lt;/p>
&lt;p>If you&amp;rsquo;re running OpenClaw with a single chat thread and wondering why it &amp;ldquo;forgets things&amp;rdquo; — this is probably your answer. Thread it. Topic it. Give each domain its own space.&lt;/p>
&lt;p>The AI doesn&amp;rsquo;t forget when you organize its memory.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Running OpenClaw? Join the community at &lt;a href="https://discord.com/invite/clawd">discord.com/invite/clawd&lt;/a>. Find skills at &lt;a href="https://clawhub.ai">clawhub.ai&lt;/a>.&lt;/em>&lt;/p></description><content:encoded>&lt;h2 id="the-problem-with-one-big-chat">The Problem with One Big Chat&lt;/h2>
&lt;p>When I first set up OpenClaw, I did what most people do — one Telegram chat, one conversation, everything in one place. It worked&amp;hellip; for about a week.&lt;/p>
&lt;p>Then things got messy. I&amp;rsquo;d ask about a Kubernetes issue and the AI would reference a recipe I&amp;rsquo;d asked about earlier. I&amp;rsquo;d try to draft a blog post and it would pull in context from a client billing discussion. The context window — the amount of conversation the AI can hold at once — was polluted with everything from smart home commands to security audits.&lt;/p>
&lt;p>The real problem isn&amp;rsquo;t that the AI is bad at context. It&amp;rsquo;s that &lt;strong>a single conversation thread forces every topic to compete for the same limited context window.&lt;/strong> When that window fills up, older messages get compressed into summaries. Summaries lose details. Your agent slowly forgets who you are, what you&amp;rsquo;re working on, and what you told it yesterday.&lt;/p>
&lt;p>I lost an entire project&amp;rsquo;s context once because of this. Spent 2 hours re-explaining preferences and decisions my agent already knew. Never again.&lt;/p>
&lt;h2 id="the-solution-telegram-forum-topics">The Solution: Telegram Forum Topics&lt;/h2>
&lt;p>Telegram has a feature called &lt;strong>Topics&lt;/strong> (or Forum mode) that turns a group into a structured space where each topic is its own isolated thread. OpenClaw supports this natively — you can configure each topic with:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Its own system prompt&lt;/strong> (personality and instructions)&lt;/li>
&lt;li>&lt;strong>Its own model&lt;/strong> (Opus for deep work, Sonnet for quick tasks)&lt;/li>
&lt;li>&lt;strong>Its own skills&lt;/strong> (only the tools that topic needs)&lt;/li>
&lt;li>&lt;strong>Its own storage&lt;/strong> (Notion page + Google Drive folder)&lt;/li>
&lt;li>&lt;strong>Its own memory path&lt;/strong> (separate daily logs per domain)&lt;/li>
&lt;li>&lt;strong>Its own session&lt;/strong> (isolated context window — zero cross-contamination)&lt;/li>
&lt;/ul>
&lt;p>Think of it as giving your AI a &lt;strong>different desk for each type of work.&lt;/strong> When you walk up to the Content desk, it&amp;rsquo;s already thinking about content. When you walk up to the Infra desk, it&amp;rsquo;s in ops mode. They don&amp;rsquo;t leak into each other.&lt;/p>
&lt;h2 id="my-setup-9-topics">My Setup: 9 Topics&lt;/h2>
&lt;p>Here&amp;rsquo;s how I have mine structured:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Topic&lt;/th>
&lt;th>Model&lt;/th>
&lt;th>Skills&lt;/th>
&lt;th>Purpose&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>📋 Mission Control&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Tasks, priorities, reminders, deadlines&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🏠 Home&lt;/td>
&lt;td>Sonnet&lt;/td>
&lt;td>sonoscli, camsnap, weather&lt;/td>
&lt;td>Smart home: Sonos, cameras, devices&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>⚙️ Infra &amp;amp; K8s&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>github, gh-issues, coding-agent, healthcheck&lt;/td>
&lt;td>Kubernetes clusters, logs, incident response&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>✍️ Content&lt;/td>
&lt;td>&lt;strong>Opus&lt;/strong>&lt;/td>
&lt;td>notion, summarize, xurl, blogwatcher&lt;/td>
&lt;td>Scripts, social posts, webinar prep&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🔍 Research&lt;/td>
&lt;td>&lt;strong>Opus&lt;/strong>&lt;/td>
&lt;td>notion, summarize, blogwatcher, github&lt;/td>
&lt;td>Deep dives on people, products, companies&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>💪 Health &amp;amp; Fitness&lt;/td>
&lt;td>Sonnet&lt;/td>
&lt;td>weather&lt;/td>
&lt;td>WHOOP data, workouts, health tracking&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🤖 AI &amp;amp; Agents&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>OpenClaw config, skill development, experiments&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>⚡ Overflow&lt;/td>
&lt;td>Sonnet&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Quick asks that don&amp;rsquo;t fit elsewhere&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>💼 Corp&lt;/td>
&lt;td>Sonnet (default)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>Client relationships, billing, time tracking&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="step-by-step-setup">Step-by-Step Setup&lt;/h2>
&lt;h3 id="1-create-the-telegram-forum-group">1. Create the Telegram Forum Group&lt;/h3>
&lt;ol>
&lt;li>Open Telegram → &lt;strong>New Group&lt;/strong> → name it (I called mine &amp;ldquo;OpenClaw&amp;rdquo;)&lt;/li>
&lt;li>Add your OpenClaw bot to the group&lt;/li>
&lt;li>Go to &lt;strong>Group Settings → Edit → Enable Topics&lt;/strong> (Forum mode)&lt;/li>
&lt;li>Create each topic with an emoji prefix for quick scanning&lt;/li>
&lt;/ol>
&lt;p>Each topic gets a numeric ID from Telegram. You&amp;rsquo;ll need these for the config.&lt;/p>
&lt;h3 id="2-get-your-ids">2. Get Your IDs&lt;/h3>
&lt;p>You need three things:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Group chat ID&lt;/strong>: the negative number for your group (check Gateway logs after sending a message)&lt;/li>
&lt;li>&lt;strong>Topic IDs&lt;/strong>: each topic&amp;rsquo;s thread number (also in logs)&lt;/li>
&lt;li>&lt;strong>Your Telegram user ID&lt;/strong>: use &lt;code>@userinfobot&lt;/code> in Telegram&lt;/li>
&lt;/ul>
&lt;h3 id="3-define-your-agents">3. Define Your Agents&lt;/h3>
&lt;p>Before configuring topics, set up agents in &lt;code>openclaw.json&lt;/code>. Each agent maps to a model:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agents: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> defaults: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> model: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> primary: &amp;#34;anthropic/claude-sonnet-4-6&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> fallbacks: [&amp;#34;anthropic/claude-opus-4-6&amp;#34;]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> list: [
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> { id: &amp;#34;main&amp;#34; },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id: &amp;#34;opus&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: &amp;#34;opus&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workspace: &amp;#34;~/.openclaw/workspace&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentDir: &amp;#34;~/.openclaw/agents/opus/agent&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> model: &amp;#34;anthropic/claude-opus-4-6&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> id: &amp;#34;sonnet&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> name: &amp;#34;sonnet&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workspace: &amp;#34;~/.openclaw/workspace&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentDir: &amp;#34;~/.openclaw/agents/sonnet/agent&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> model: &amp;#34;anthropic/claude-sonnet-4-6&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key decisions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>All agents share the same workspace&lt;/strong> — so &lt;code>SOUL.md&lt;/code>, &lt;code>AGENTS.md&lt;/code>, and &lt;code>MEMORY.md&lt;/code> are consistent&lt;/li>
&lt;li>&lt;strong>Each agent gets its own &lt;code>agentDir&lt;/code>&lt;/strong> — for separate auth profiles and sessions&lt;/li>
&lt;li>&lt;strong>Sonnet is the default&lt;/strong> — fast and cheap for 80% of tasks&lt;/li>
&lt;li>&lt;strong>Opus is reserved&lt;/strong> — only for topics that need deep reasoning or creativity&lt;/li>
&lt;/ul>
&lt;h3 id="4-configure-the-telegram-group">4. Configure the Telegram Group&lt;/h3>
&lt;p>Here&amp;rsquo;s the structure inside &lt;code>openclaw.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> telegram: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> enabled: true,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> botToken: &amp;#34;YOUR_BOT_TOKEN&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> dmPolicy: &amp;#34;pairing&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groupPolicy: &amp;#34;allowlist&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groups: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> // Default for ALL groups: require @mention
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;*&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: true
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> // Your specific forum group
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;-1003885751519&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowFrom: [&amp;#34;775644809&amp;#34;], // Only YOUR Telegram user ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> topics: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> // Each topic goes here...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="5-configure-each-topic">5. Configure Each Topic&lt;/h3>
&lt;p>Every topic follows the same pattern:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;#34;TOPIC_ID&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentId: &amp;#34;opus&amp;#34;, // or &amp;#34;sonnet&amp;#34;, or omit for default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> skills: [&amp;#34;skill-a&amp;#34;, &amp;#34;skill-b&amp;#34;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> systemPrompt: &amp;#34;MISSION + STYLE + STORAGE + MEMORY&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here&amp;rsquo;s a real example — my Content topic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;#34;48&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> agentId: &amp;#34;opus&amp;#34;,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> skills: [&amp;#34;notion&amp;#34;, &amp;#34;summarize&amp;#34;, &amp;#34;xurl&amp;#34;, &amp;#34;blogwatcher&amp;#34;],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> systemPrompt: &amp;#34;You help draft scripts, social posts, and webinar prep. Match the user&amp;#39;s voice and tone. Ask clarifying questions before producing long-form content.\n\nStorage:\n- Notion page: 32d4d457-9015-8138-ba2d-e40df5d4b20e (Content)\n- Google Drive folder: 1__Z8KpROUZucvWbH-4cRzw8tA1BhWFDM (Content)\nUse Notion for drafts, content calendar, and outlines. Use Drive for final assets, slides, and media.\n\nDaily memory: Write dated notes to `memory/content/YYYY-MM-DD.md` for this topic.&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="the-system-prompt-pattern">The System Prompt Pattern&lt;/h3>
&lt;p>Every topic system prompt follows four parts:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>MISSION&lt;/strong> — What this topic does (1-2 sentences)&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;You manage tasks, priorities, and reminders.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>STYLE&lt;/strong> — How to respond&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Be proactive about deadlines. Keep answers practical.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>STORAGE&lt;/strong> — Where to save work&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Notion page: &lt;code>&amp;lt;ID&amp;gt;&lt;/code> (Mission Control) / Drive folder: &lt;code>&amp;lt;ID&amp;gt;&lt;/code>&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>MEMORY&lt;/strong> — Where to write daily logs&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Write dated notes to &lt;code>memory/mission-control/YYYY-MM-DD.md&lt;/code>&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;/li>
&lt;/ol>
&lt;p>This ensures every topic knows its job, its voice, where to put things, and where to journal. When you open a topic, the AI is already in the right headspace.&lt;/p>
&lt;h2 id="model-routing-the-strategy">Model Routing: The Strategy&lt;/h2>
&lt;p>Not every task needs the most expensive model. Here&amp;rsquo;s my routing logic:&lt;/p>
&lt;p>&lt;strong>Use Opus (frontier model) when:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Creative writing — blog posts, scripts, social content&lt;/li>
&lt;li>Deep research — connecting non-obvious dots across sources&lt;/li>
&lt;li>Complex reasoning — architecture decisions, strategy&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Use Sonnet (fast/cheap) for everything else:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Task management — creating, updating, tracking tasks&lt;/li>
&lt;li>Smart home — &amp;ldquo;play jazz in the kitchen&amp;rdquo; doesn&amp;rsquo;t need genius&lt;/li>
&lt;li>Health data — querying WHOOP stats is structured, not creative&lt;/li>
&lt;li>Quick asks — definitions, lookups, simple questions&lt;/li>
&lt;li>Ops work — kubectl commands, log parsing, incident response&lt;/li>
&lt;/ul>
&lt;p>The result: &lt;strong>~80% of my interactions use Sonnet&lt;/strong> (faster, cheaper), and the 20% that use Opus actually benefit from it.&lt;/p>
&lt;h2 id="skills-routing-keep-it-lean">Skills Routing: Keep It Lean&lt;/h2>
&lt;p>Each topic only loads the skills it needs. This matters because:&lt;/p>
&lt;ul>
&lt;li>Every skill adds instructions to the system prompt&lt;/li>
&lt;li>More instructions = more tokens consumed per turn&lt;/li>
&lt;li>More tokens = faster context window fill-up = more compaction = more forgetting&lt;/li>
&lt;/ul>
&lt;p>My mapping:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Topic&lt;/th>
&lt;th>Skills&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>🏠 Home&lt;/td>
&lt;td>sonoscli, camsnap, weather&lt;/td>
&lt;td>Smart device control&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>⚙️ Infra&lt;/td>
&lt;td>github, gh-issues, coding-agent, healthcheck&lt;/td>
&lt;td>DevOps workflow&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>✍️ Content&lt;/td>
&lt;td>notion, summarize, xurl, blogwatcher&lt;/td>
&lt;td>Content pipeline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>🔍 Research&lt;/td>
&lt;td>notion, summarize, blogwatcher, github&lt;/td>
&lt;td>Research tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>💪 Health&lt;/td>
&lt;td>weather&lt;/td>
&lt;td>Outdoor planning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Everything else&lt;/td>
&lt;td>&lt;em>(none)&lt;/em>&lt;/td>
&lt;td>Keep it lean&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Rule of thumb:&lt;/strong> if a skill isn&amp;rsquo;t used weekly in a topic, remove it.&lt;/p>
&lt;h2 id="memory-separate-paths-per-domain">Memory: Separate Paths Per Domain&lt;/h2>
&lt;p>This is the key insight that makes the whole architecture work. Each topic writes its daily logs to a &lt;strong>separate memory subdirectory&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">memory/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── mission-control/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── home/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── infra/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── content/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── research/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── health/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── ai/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── corp/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── general/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── 2026-03-29.md
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why this matters:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No cross-contamination&lt;/strong> — infra notes don&amp;rsquo;t pollute your content drafts&lt;/li>
&lt;li>&lt;strong>Vector search stays relevant&lt;/strong> — when searching memory from the Content topic, you find content notes, not Kubernetes debugging logs&lt;/li>
&lt;li>&lt;strong>Compaction is less painful&lt;/strong> — each topic&amp;rsquo;s session is smaller and more focused, so compaction happens less often and loses less context&lt;/li>
&lt;/ul>
&lt;p>Combined with &lt;code>MEMORY.md&lt;/code> for permanent long-term storage (key facts, project overviews, infrastructure details), this creates a layered memory system:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MEMORY.md&lt;/strong> — permanent, cross-topic, loaded every session&lt;/li>
&lt;li>&lt;strong>memory/&lt;domain>/YYYY-MM-DD.md&lt;/strong> — daily, topic-specific, searched on demand&lt;/li>
&lt;/ul>
&lt;h2 id="security-three-layers">Security: Three Layers&lt;/h2>
&lt;p>The config implements three security layers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groupPolicy: &amp;#34;allowlist&amp;#34;, // Layer 1: only approved groups
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> groups: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;*&amp;#34;: { requireMention: true }, // Layer 2: strangers must @mention
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &amp;#34;-1003885751519&amp;#34;: {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> requireMention: false,
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> allowFrom: [&amp;#34;775644809&amp;#34;] // Layer 3: only YOUR user ID
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> },
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> dmPolicy: &amp;#34;pairing&amp;#34; // DMs require pairing code
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Even if someone joins your Telegram group:&lt;/p>
&lt;ul>
&lt;li>They can&amp;rsquo;t trigger the bot in other groups (&lt;code>groupPolicy: &amp;quot;allowlist&amp;quot;&lt;/code>)&lt;/li>
&lt;li>They&amp;rsquo;d need to @mention the bot in random groups (&lt;code>requireMention: true&lt;/code> on &lt;code>&amp;quot;*&amp;quot;&lt;/code>)&lt;/li>
&lt;li>They can&amp;rsquo;t trigger actions in your forum (&lt;code>allowFrom&lt;/code> restricts to your user ID)&lt;/li>
&lt;/ul>
&lt;h2 id="storage-per-topic-notion--drive">Storage: Per-Topic Notion + Drive&lt;/h2>
&lt;p>Each topic has its own Notion page and Google Drive folder. This means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Content topic&lt;/strong> → drafts go to the Content Notion page, final slides go to the Content Drive folder&lt;/li>
&lt;li>&lt;strong>Infra topic&lt;/strong> → runbooks go to the Infra Notion page, log dumps go to the Infra Drive folder&lt;/li>
&lt;li>&lt;strong>Corp topic&lt;/strong> → client notes go to the Corp Notion page, contracts go to the Corp Drive folder&lt;/li>
&lt;/ul>
&lt;p>No hunting. No &amp;ldquo;which folder was that in?&amp;rdquo; Everything is organized by domain from the moment it&amp;rsquo;s created.&lt;/p>
&lt;h2 id="tips-from-3-months-of-use">Tips From 3+ Months of Use&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Start with 3-4 topics&lt;/strong> — add more when you find natural boundaries&lt;/li>
&lt;li>&lt;strong>Overflow is essential&lt;/strong> — random questions need somewhere to go without polluting focused topics&lt;/li>
&lt;li>&lt;strong>Emoji prefixes matter&lt;/strong> — when you have 9 topics, visual scanning speed matters&lt;/li>
&lt;li>&lt;strong>Review monthly&lt;/strong> — merge underused topics, split overloaded ones&lt;/li>
&lt;li>&lt;strong>Pin important messages&lt;/strong> in each topic for quick reference&lt;/li>
&lt;li>&lt;strong>Keep system prompts under 500 words&lt;/strong> — they&amp;rsquo;re injected every turn&lt;/li>
&lt;li>&lt;strong>Memory subdirectories are non-negotiable&lt;/strong> — this is what prevents context bleed&lt;/li>
&lt;li>&lt;strong>Use a Meta topic&lt;/strong> (my &amp;ldquo;AI &amp;amp; Agents&amp;rdquo;) for discussing the setup itself&lt;/li>
&lt;li>&lt;strong>Back up &lt;code>openclaw.json&lt;/code>&lt;/strong> — it&amp;rsquo;s the single source of truth for this entire architecture&lt;/li>
&lt;li>&lt;strong>The cheapest model that works is the right model&lt;/strong> — don&amp;rsquo;t waste Opus on &amp;ldquo;what&amp;rsquo;s the weather&amp;rdquo;&lt;/li>
&lt;/ol>
&lt;h2 id="the-complete-config">The Complete Config&lt;/h2>
&lt;p>Here&amp;rsquo;s the full relevant section of my &lt;code>openclaw.json&lt;/code> for reference:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">telegram&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">enabled&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">botToken&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;YOUR_BOT_TOKEN&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dmPolicy&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;pairing&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">groupPolicy&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;allowlist&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">streaming&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;off&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">groups&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">true&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;-YOUR_GROUP_ID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">allowFrom&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;YOUR_USER_ID&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">topics&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;45&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You manage tasks, priorities, and reminders. Be proactive about deadlines and follow-ups. Track what&amp;#39;s pending and surface what needs attention.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Mission Control)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Mission Control)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">Use Notion for tasks, SOPs, and structured notes. Use Drive for file attachments and exports.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/mission-control/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;46&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;sonoscli&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;camsnap&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;weather&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You assist with home automation: Sonos, cameras, and smart devices. Keep answers practical and action-oriented.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Home)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Home)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/home/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;47&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;gh-issues&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;coding-agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;healthcheck&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You assist with Kubernetes clusters, logs, and incident response. Be concise and ops-focused. Prefer actionable commands over lengthy explanations.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Infra &amp;amp; K8s)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Infra &amp;amp; K8s)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/infra/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;48&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;opus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;notion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;summarize&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;xurl&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;blogwatcher&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You help draft scripts, social posts, and webinar prep. Match the user&amp;#39;s voice and tone. Ask clarifying questions before producing long-form content.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Content)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Content)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/content/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;49&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;opus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;notion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;summarize&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;blogwatcher&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You research people, products, companies, and ideas. Cite sources, be thorough, and surface non-obvious connections.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Research)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Research)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/research/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;50&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">skills&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;weather&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You help with WHOOP data, workout planning, and health dashboard work. Be data-driven and concise.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Health &amp;amp; Fitness)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Health &amp;amp; Fitness)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/health/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;51&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You help with OpenClaw configuration, skill development, and agent experiments. You can be technical and detailed here.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (AI &amp;amp; Agents)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (AI &amp;amp; Agents)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/ai/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;66&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentId&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You handle overflow and quick asks. Keep responses short and direct.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/general/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;387&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">requireMention&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">systemPrompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;You manage customer relationships, project billing, and time tracking. Focus on: active clients, logged hours, invoices, and follow-ups. Be direct — flag unbilled time, overdue invoices, and upcoming deadlines.&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Storage:&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Notion page: &amp;lt;ID&amp;gt; (Corp)&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">- Google Drive folder: &amp;lt;ID&amp;gt; (Corp)&lt;/span>&lt;span class="se">\n\n&lt;/span>&lt;span class="s2">Daily memory: Write dated notes to `memory/corp/YYYY-MM-DD.md` for this topic.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agents&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">defaults&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">primary&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;anthropic/claude-sonnet-4-6&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">fallbacks&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;anthropic/claude-opus-4-6&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">list&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;main&amp;#34;&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;opus&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">workspace&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentDir&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/agents/opus/agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;anthropic/claude-opus-4-6&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sonnet&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">workspace&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/workspace&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">agentDir&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;~/.openclaw/agents/sonnet/agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;anthropic/claude-sonnet-4-6&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;p>This setup has transformed how I interact with AI. Instead of one overwhelmed assistant trying to be everything, I have a team of specialists — each focused, each equipped, each with its own memory. The context window problem is essentially solved because no single topic accumulates enough history to trigger aggressive compaction.&lt;/p>
&lt;p>If you&amp;rsquo;re running OpenClaw with a single chat thread and wondering why it &amp;ldquo;forgets things&amp;rdquo; — this is probably your answer. Thread it. Topic it. Give each domain its own space.&lt;/p>
&lt;p>The AI doesn&amp;rsquo;t forget when you organize its memory.&lt;/p>
&lt;hr>
&lt;p>&lt;em>Running OpenClaw? Join the community at &lt;a href="https://discord.com/invite/clawd">discord.com/invite/clawd&lt;/a>. Find skills at &lt;a href="https://clawhub.ai">clawhub.ai&lt;/a>.&lt;/em>&lt;/p></content:encoded></item><item><title>Building a Slack Bot That Updates Your Docs: Kagent, MCP, and GitHub in Action</title><link>https://maniak.io/articles/2026-03-20-building-a-slack-bot-that-updates-your-docs-kagent-mcp-github/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-03-20-building-a-slack-bot-that-updates-your-docs-kagent-mcp-github/</guid><description>&lt;h2 id="the-problem-nobody-talks-about">The Problem Nobody Talks About&lt;/h2>
&lt;p>Documentation rots. Everyone knows it, nobody wants to fix it. The friction is real — switching context from a Slack conversation about a bug to opening a browser, navigating to a repo, editing markdown, committing, pushing, and opening a PR. By the time you&amp;rsquo;ve done all that, the conversation has moved on and the motivation is gone.&lt;/p>
&lt;p>What if updating docs was as simple as telling a bot in Slack, &amp;ldquo;Hey, update the getting started guide to mention the new evaluator type&amp;rdquo;?&lt;/p>
&lt;p>That&amp;rsquo;s exactly what this project does. A Slack bot, running inside Kubernetes, powered by an AI agent framework called Kagent, connected to GitHub through the Model Context Protocol (MCP). You talk to it in Slack. It reads the docs, makes changes, opens PRs — and asks for your approval before doing anything destructive.&lt;/p>
&lt;p>No more context switching. No more stale docs.&lt;/p>
&lt;hr>
&lt;h2 id="why-this-exists">Why This Exists&lt;/h2>
&lt;p>Three pain points drove this build:&lt;/p>
&lt;p>&lt;strong>1. Teams live in Slack.&lt;/strong> That&amp;rsquo;s where decisions happen, where context lives, where people already are. Forcing someone out of Slack to manage docs is a guaranteed way to ensure docs never get updated.&lt;/p>
&lt;p>&lt;strong>2. LLMs are good at writing, bad at acting.&lt;/strong> ChatGPT can draft a paragraph, sure. But it can&amp;rsquo;t read your existing docs, understand the structure, create a branch, commit the change, and open a PR. You need tools — real, connected tools — not just text generation.&lt;/p>
&lt;p>&lt;strong>3. Trust requires guardrails.&lt;/strong> Nobody wants an AI silently pushing code to main. The agent needs to ask permission before it mutates anything. Human-in-the-loop isn&amp;rsquo;t a nice-to-have, it&amp;rsquo;s a requirement.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture">The Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s the high-level flow:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Slack&lt;/span> &lt;span class="n">message&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Slack&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Socket&lt;/span> &lt;span class="n">Mode&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="n">ingress&lt;/span> &lt;span class="n">required&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Kagent&lt;/span> &lt;span class="n">Controller&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">A2A&lt;/span> &lt;span class="n">protocol&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Agent&lt;/span> &lt;span class="n">with&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">├──&lt;/span> &lt;span class="n">slack&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">mcp&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">Post&lt;/span> &lt;span class="n">status&lt;/span> &lt;span class="n">updates&lt;/span> &lt;span class="n">back&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">Slack&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──&lt;/span> &lt;span class="n">github&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">mcp&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">Read&lt;/span> &lt;span class="n">files&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">branches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">commit&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">open&lt;/span> &lt;span class="n">PRs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">GitHub&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">via&lt;/span> &lt;span class="n">Copilot&lt;/span> &lt;span class="n">hosted&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">endpoint&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The entire stack runs on Kubernetes. No external servers, no Lambda functions, no third-party SaaS platforms mediating the conversation. Everything is declared as Kubernetes resources, managed through GitOps, and secured with proper secrets management.&lt;/p>
&lt;hr>
&lt;h2 id="what-is-kagent">What is Kagent?&lt;/h2>
&lt;p>Kagent is a Kubernetes-native AI agent framework. Think of it as a controller that manages AI agents the same way Kubernetes manages pods — through declarative YAML manifests.&lt;/p>
&lt;p>Instead of writing a Python script that calls OpenAI and bolts on some tools, you declare an &lt;code>Agent&lt;/code> custom resource:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docs-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">MCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">slack-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Kagent handles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM routing&lt;/strong> — Which model to call, with what credentials&lt;/li>
&lt;li>&lt;strong>Tool orchestration&lt;/strong> — MCP servers are mounted as tools the agent can invoke&lt;/li>
&lt;li>&lt;strong>Memory&lt;/strong> — Embedding-based long-term context so the agent remembers past conversations&lt;/li>
&lt;li>&lt;strong>Context compaction&lt;/strong> — When conversations get long, Kagent compresses earlier messages to stay within token limits&lt;/li>
&lt;li>&lt;strong>A2A protocol&lt;/strong> — Agent-to-Agent communication, which is how the Slack bot talks to the Kagent controller&lt;/li>
&lt;/ul>
&lt;p>The agent isn&amp;rsquo;t a monolith. It&amp;rsquo;s a composition of capabilities declared in YAML and reconciled by a Kubernetes controller. Add a new tool? Add a line to the manifest. Change the model? Update a config reference. Everything follows the Kubernetes pattern — desired state in git, actual state in the cluster, a controller reconciling the difference.&lt;/p>
&lt;hr>
&lt;h2 id="how-github-mcp-server-fits-in">How GitHub MCP Server Fits In&lt;/h2>
&lt;p>The Model Context Protocol (MCP) is a standard for giving AI agents access to tools. Instead of writing custom integrations for every service, you expose tools through an MCP server, and any MCP-compatible agent can use them.&lt;/p>
&lt;p>For GitHub, this means the agent gets access to operations like:&lt;/p>
&lt;ul>
&lt;li>&lt;code>get_file_contents&lt;/code> — Read any file in a repository&lt;/li>
&lt;li>&lt;code>create_or_update_file&lt;/code> — Edit files and commit changes&lt;/li>
&lt;li>&lt;code>create_branch&lt;/code> — Work on feature branches, not main&lt;/li>
&lt;li>&lt;code>create_pull_request&lt;/code> — Open PRs for review&lt;/li>
&lt;li>&lt;code>search_code&lt;/code> — Find relevant files across the repo&lt;/li>
&lt;li>&lt;code>list_issues&lt;/code>, &lt;code>create_issue&lt;/code> — Manage issues alongside docs&lt;/li>
&lt;/ul>
&lt;p>The GitHub MCP server is declared as a &lt;code>RemoteMCPServer&lt;/code> resource in Kubernetes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">transport&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">STREAMABLE_HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">streamableHTTP&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://api.githubcopilot.com/mcp/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headersFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Authorization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-pat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things worth noting:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No self-hosted binary.&lt;/strong> This uses GitHub Copilot&amp;rsquo;s hosted MCP endpoint. No containers to build, no sidecars to manage.&lt;/li>
&lt;li>&lt;strong>Auth through Kubernetes secrets.&lt;/strong> The GitHub PAT is stored in HashiCorp Vault, synced to a Kubernetes secret via External Secrets Operator, and injected as a Bearer token header. The agent never sees the raw token.&lt;/li>
&lt;li>&lt;strong>Streamable HTTP transport.&lt;/strong> The MCP server uses HTTP with streaming support, so long-running operations don&amp;rsquo;t time out.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="the-slack-bot-bridging-chat-and-code">The Slack Bot: Bridging Chat and Code&lt;/h2>
&lt;p>The Slack bot is the user-facing piece. It runs in Socket Mode — meaning it opens a websocket connection to Slack&amp;rsquo;s API rather than requiring an inbound webhook URL. This is a deliberate choice: no ingress controller needed, no public endpoints exposed, no certificates to manage.&lt;/p>
&lt;p>When a message comes in, the bot forwards it to the Kagent controller using the A2A (Agent-to-Agent) protocol over HTTP:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AsyncClient&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">KAGENT_BASE_URL&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/api/a2a/kagent/&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">AGENT_NAME&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">payload&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The agent processes the request — which might involve reading docs from GitHub, drafting changes, and preparing a PR — then returns a response. But here&amp;rsquo;s the critical part: &lt;strong>mutating operations require human approval.&lt;/strong>&lt;/p>
&lt;p>When the agent wants to run &lt;code>create_or_update_file&lt;/code>, &lt;code>push_files&lt;/code>, &lt;code>create_pull_request&lt;/code>, or &lt;code>merge_pull_request&lt;/code>, the Slack bot renders an approval prompt:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Tool: create_pull_request&lt;/strong>
Repository: my-org/website
Title: &amp;ldquo;Update getting started guide with new evaluator type&amp;rdquo;
Branch: docs/update-evaluators&lt;/p>
&lt;p>[ Approve ] [ Deny ]&lt;/p>
&lt;/blockquote>
&lt;p>The user clicks a button in Slack. Only then does the agent execute the action. This isn&amp;rsquo;t just a safety measure — it&amp;rsquo;s a trust-building mechanism. People adopt tools they can control.&lt;/p>
&lt;hr>
&lt;h2 id="secrets-management-the-boring-part-that-matters">Secrets Management: The Boring Part That Matters&lt;/h2>
&lt;p>Every credential in this system flows through a single pipeline:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">HashiCorp Vault → External Secrets Operator → Kubernetes Secret → Application
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Vault stores the truth:&lt;/p>
&lt;ul>
&lt;li>LLM API keys at &lt;code>secret/kagent/llm&lt;/code>&lt;/li>
&lt;li>Slack bot tokens at &lt;code>secret/slack&lt;/code>&lt;/li>
&lt;li>GitHub PAT at &lt;code>secret/github&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>External Secrets Operator polls Vault every hour and syncs secrets into Kubernetes. Applications reference Kubernetes secrets in their manifests. Nobody hardcodes a token. Nobody copy-pastes a key into a YAML file.&lt;/p>
&lt;p>The bootstrap script initializes everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Initialize Vault, configure K8s auth, create policies&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./bootstrap/vault-init.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It prompts for each secret interactively, stores them in Vault, and configures the Kubernetes auth method so ESO can retrieve them. One script, one time, all secrets in the right place.&lt;/p>
&lt;hr>
&lt;h2 id="deployment-gitops-all-the-way-down">Deployment: GitOps All the Way Down&lt;/h2>
&lt;p>The entire stack deploys through Argo CD using an App of Apps pattern with sync waves:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Wave&lt;/th>
&lt;th>What Deploys&lt;/th>
&lt;th>Why This Order&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>Vault, External Secrets Operator&lt;/td>
&lt;td>Secrets infrastructure first&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1-3&lt;/td>
&lt;td>Istio (ambient mesh)&lt;/td>
&lt;td>Zero-trust networking&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Kagent CRDs&lt;/td>
&lt;td>Agent framework schema&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>Kagent controller&lt;/td>
&lt;td>Agent runtime&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>Vault config (SecretStores, ExternalSecrets)&lt;/td>
&lt;td>Wire secrets to apps&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>Slack bot, GitHub agent&lt;/td>
&lt;td>Application layer&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>One &lt;code>git push&lt;/code> to main and Argo CD reconciles everything. Auto-prune removes deleted resources. Auto-heal fixes drift. If someone &lt;code>kubectl delete&lt;/code>s a resource, Argo puts it back.&lt;/p>
&lt;p>The bootstrap is two commands:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./bootstrap/install.sh &lt;span class="c1"># Argo CD + Gateway API CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./bootstrap/vault-init.sh &lt;span class="c1"># Secrets initialization&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After that, git is the only interface.&lt;/p>
&lt;hr>
&lt;h2 id="what-a-real-interaction-looks-like">What a Real Interaction Looks Like&lt;/h2>
&lt;p>Here&amp;rsquo;s a realistic conversation in Slack:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> The docs for the scoring evaluator are missing the threshold parameter. Can you add it to the reference page?&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> I&amp;rsquo;ll check the current docs. Let me read the evaluator reference page.&lt;/p>
&lt;p>&lt;em>(Agent calls &lt;code>get_file_contents&lt;/code> on the docs repo)&lt;/em>&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> Found it. The scoring evaluator section at &lt;code>content/docs/evaluators/scoring.md&lt;/code> doesn&amp;rsquo;t mention the &lt;code>threshold&lt;/code> parameter. I&amp;rsquo;ll create a branch and update the docs.&lt;/p>
&lt;p>&lt;em>(Agent calls &lt;code>create_branch&lt;/code> → &lt;code>create_or_update_file&lt;/code>)&lt;/em>&lt;/p>
&lt;p>&lt;strong>⚠️ Approval Required&lt;/strong>
&lt;strong>Tool:&lt;/strong> create_or_update_file
&lt;strong>File:&lt;/strong> content/docs/evaluators/scoring.md
&lt;strong>Branch:&lt;/strong> docs/add-threshold-param&lt;/p>
&lt;p>[ Approve ] [ Deny ]&lt;/p>
&lt;p>&lt;strong>You:&lt;/strong> &lt;em>clicks Approve&lt;/em>&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> Done. I&amp;rsquo;ve also opened a PR for review.&lt;/p>
&lt;p>&lt;strong>⚠️ Approval Required&lt;/strong>
&lt;strong>Tool:&lt;/strong> create_pull_request
&lt;strong>Title:&lt;/strong> &amp;ldquo;Add threshold parameter to scoring evaluator docs&amp;rdquo;&lt;/p>
&lt;p>[ Approve ] [ Deny ]&lt;/p>
&lt;p>&lt;strong>You:&lt;/strong> &lt;em>clicks Approve&lt;/em>&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> PR #42 is open. Here&amp;rsquo;s the link: [PR #42]&lt;/p>
&lt;/blockquote>
&lt;p>Total time: under a minute. No browser tabs opened. No git commands typed. No context lost.&lt;/p>
&lt;hr>
&lt;h2 id="lessons-learned">Lessons Learned&lt;/h2>
&lt;p>&lt;strong>Socket Mode is underrated.&lt;/strong> Not needing ingress for a Slack bot simplifies everything. No TLS certs, no DNS records, no service exposure. The bot dials out; Slack sends messages over the websocket. For internal tools, this is the way.&lt;/p>
&lt;p>&lt;strong>MCP is the right abstraction for agent tools.&lt;/strong> Before MCP, every AI agent had bespoke tool integrations — custom functions wrapping API calls. MCP standardizes this. Swap GitHub for GitLab? Replace one MCP server, the agent doesn&amp;rsquo;t change. The protocol decouples the agent from the tools.&lt;/p>
&lt;p>&lt;strong>Human-in-the-loop needs good UX.&lt;/strong> A wall of JSON asking &amp;ldquo;approve this?&amp;rdquo; doesn&amp;rsquo;t cut it. The approval prompt needs to clearly show what action is being taken, on what resource, with what parameters. Slack&amp;rsquo;s interactive blocks make this possible — buttons, structured messages, and threaded conversations.&lt;/p>
&lt;p>&lt;strong>GitOps + AI agents work well together.&lt;/strong> The agent configurations are YAML in git. The secrets pipeline is declarative. The deployment order is encoded in sync waves. When something breaks, &lt;code>git log&lt;/code> tells you what changed. When you want to add a new agent, you add manifests and push. The operational model is the same as any other Kubernetes workload.&lt;/p>
&lt;p>&lt;strong>Start with read-only, add writes carefully.&lt;/strong> The first version of this agent could only read docs and post to Slack. Writes came later, gated behind approvals. This incremental approach builds confidence — both in the system and in the team using it.&lt;/p>
&lt;hr>
&lt;h2 id="try-it-yourself">Try It Yourself&lt;/h2>
&lt;p>The building blocks are all open source:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://github.com/kagent-dev/kagent">Kagent&lt;/a>&lt;/strong> — Kubernetes-native AI agent framework&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://api.githubcopilot.com/mcp/">GitHub MCP Server&lt;/a>&lt;/strong> — GitHub&amp;rsquo;s hosted MCP endpoint (requires a GitHub PAT)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://slack.dev/bolt-python/">Slack Bolt&lt;/a>&lt;/strong> — Python framework for Slack apps with Socket Mode support&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://external-secrets.io/">External Secrets Operator&lt;/a>&lt;/strong> — Sync secrets from Vault (or other providers) to Kubernetes&lt;/li>
&lt;/ul>
&lt;p>The pattern generalizes beyond documentation. Any workflow that involves reading from a system, deciding on an action, getting human approval, and executing — that&amp;rsquo;s an agent use case. Incident response, infrastructure changes, onboarding checklists. The Slack bot is the interface. Kagent is the brain. MCP servers are the hands.&lt;/p>
&lt;p>The gap between &amp;ldquo;we should update the docs&amp;rdquo; and &amp;ldquo;the docs are updated&amp;rdquo; just got a lot smaller.&lt;/p></description><content:encoded>&lt;h2 id="the-problem-nobody-talks-about">The Problem Nobody Talks About&lt;/h2>
&lt;p>Documentation rots. Everyone knows it, nobody wants to fix it. The friction is real — switching context from a Slack conversation about a bug to opening a browser, navigating to a repo, editing markdown, committing, pushing, and opening a PR. By the time you&amp;rsquo;ve done all that, the conversation has moved on and the motivation is gone.&lt;/p>
&lt;p>What if updating docs was as simple as telling a bot in Slack, &amp;ldquo;Hey, update the getting started guide to mention the new evaluator type&amp;rdquo;?&lt;/p>
&lt;p>That&amp;rsquo;s exactly what this project does. A Slack bot, running inside Kubernetes, powered by an AI agent framework called Kagent, connected to GitHub through the Model Context Protocol (MCP). You talk to it in Slack. It reads the docs, makes changes, opens PRs — and asks for your approval before doing anything destructive.&lt;/p>
&lt;p>No more context switching. No more stale docs.&lt;/p>
&lt;hr>
&lt;h2 id="why-this-exists">Why This Exists&lt;/h2>
&lt;p>Three pain points drove this build:&lt;/p>
&lt;p>&lt;strong>1. Teams live in Slack.&lt;/strong> That&amp;rsquo;s where decisions happen, where context lives, where people already are. Forcing someone out of Slack to manage docs is a guaranteed way to ensure docs never get updated.&lt;/p>
&lt;p>&lt;strong>2. LLMs are good at writing, bad at acting.&lt;/strong> ChatGPT can draft a paragraph, sure. But it can&amp;rsquo;t read your existing docs, understand the structure, create a branch, commit the change, and open a PR. You need tools — real, connected tools — not just text generation.&lt;/p>
&lt;p>&lt;strong>3. Trust requires guardrails.&lt;/strong> Nobody wants an AI silently pushing code to main. The agent needs to ask permission before it mutates anything. Human-in-the-loop isn&amp;rsquo;t a nice-to-have, it&amp;rsquo;s a requirement.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture">The Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s the high-level flow:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">Slack&lt;/span> &lt;span class="n">message&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Slack&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Socket&lt;/span> &lt;span class="n">Mode&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">no&lt;/span> &lt;span class="n">ingress&lt;/span> &lt;span class="n">required&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Kagent&lt;/span> &lt;span class="n">Controller&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">A2A&lt;/span> &lt;span class="n">protocol&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Agent&lt;/span> &lt;span class="n">with&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">tools&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">├──&lt;/span> &lt;span class="n">slack&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">mcp&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">Post&lt;/span> &lt;span class="n">status&lt;/span> &lt;span class="n">updates&lt;/span> &lt;span class="n">back&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">Slack&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──&lt;/span> &lt;span class="n">github&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">mcp&lt;/span> &lt;span class="err">→&lt;/span> &lt;span class="n">Read&lt;/span> &lt;span class="n">files&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">create&lt;/span> &lt;span class="n">branches&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">commit&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">open&lt;/span> &lt;span class="n">PRs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">↓&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">GitHub&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">via&lt;/span> &lt;span class="n">Copilot&lt;/span> &lt;span class="n">hosted&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">endpoint&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The entire stack runs on Kubernetes. No external servers, no Lambda functions, no third-party SaaS platforms mediating the conversation. Everything is declared as Kubernetes resources, managed through GitOps, and secured with proper secrets management.&lt;/p>
&lt;hr>
&lt;h2 id="what-is-kagent">What is Kagent?&lt;/h2>
&lt;p>Kagent is a Kubernetes-native AI agent framework. Think of it as a controller that manages AI agents the same way Kubernetes manages pods — through declarative YAML manifests.&lt;/p>
&lt;p>Instead of writing a Python script that calls OpenAI and bolts on some tools, you declare an &lt;code>Agent&lt;/code> custom resource:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docs-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">MCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">slack-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Kagent handles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM routing&lt;/strong> — Which model to call, with what credentials&lt;/li>
&lt;li>&lt;strong>Tool orchestration&lt;/strong> — MCP servers are mounted as tools the agent can invoke&lt;/li>
&lt;li>&lt;strong>Memory&lt;/strong> — Embedding-based long-term context so the agent remembers past conversations&lt;/li>
&lt;li>&lt;strong>Context compaction&lt;/strong> — When conversations get long, Kagent compresses earlier messages to stay within token limits&lt;/li>
&lt;li>&lt;strong>A2A protocol&lt;/strong> — Agent-to-Agent communication, which is how the Slack bot talks to the Kagent controller&lt;/li>
&lt;/ul>
&lt;p>The agent isn&amp;rsquo;t a monolith. It&amp;rsquo;s a composition of capabilities declared in YAML and reconciled by a Kubernetes controller. Add a new tool? Add a line to the manifest. Change the model? Update a config reference. Everything follows the Kubernetes pattern — desired state in git, actual state in the cluster, a controller reconciling the difference.&lt;/p>
&lt;hr>
&lt;h2 id="how-github-mcp-server-fits-in">How GitHub MCP Server Fits In&lt;/h2>
&lt;p>The Model Context Protocol (MCP) is a standard for giving AI agents access to tools. Instead of writing custom integrations for every service, you expose tools through an MCP server, and any MCP-compatible agent can use them.&lt;/p>
&lt;p>For GitHub, this means the agent gets access to operations like:&lt;/p>
&lt;ul>
&lt;li>&lt;code>get_file_contents&lt;/code> — Read any file in a repository&lt;/li>
&lt;li>&lt;code>create_or_update_file&lt;/code> — Edit files and commit changes&lt;/li>
&lt;li>&lt;code>create_branch&lt;/code> — Work on feature branches, not main&lt;/li>
&lt;li>&lt;code>create_pull_request&lt;/code> — Open PRs for review&lt;/li>
&lt;li>&lt;code>search_code&lt;/code> — Find relevant files across the repo&lt;/li>
&lt;li>&lt;code>list_issues&lt;/code>, &lt;code>create_issue&lt;/code> — Manage issues alongside docs&lt;/li>
&lt;/ul>
&lt;p>The GitHub MCP server is declared as a &lt;code>RemoteMCPServer&lt;/code> resource in Kubernetes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">transport&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">STREAMABLE_HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">streamableHTTP&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://api.githubcopilot.com/mcp/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headersFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Authorization&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">github-pat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few things worth noting:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No self-hosted binary.&lt;/strong> This uses GitHub Copilot&amp;rsquo;s hosted MCP endpoint. No containers to build, no sidecars to manage.&lt;/li>
&lt;li>&lt;strong>Auth through Kubernetes secrets.&lt;/strong> The GitHub PAT is stored in HashiCorp Vault, synced to a Kubernetes secret via External Secrets Operator, and injected as a Bearer token header. The agent never sees the raw token.&lt;/li>
&lt;li>&lt;strong>Streamable HTTP transport.&lt;/strong> The MCP server uses HTTP with streaming support, so long-running operations don&amp;rsquo;t time out.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="the-slack-bot-bridging-chat-and-code">The Slack Bot: Bridging Chat and Code&lt;/h2>
&lt;p>The Slack bot is the user-facing piece. It runs in Socket Mode — meaning it opens a websocket connection to Slack&amp;rsquo;s API rather than requiring an inbound webhook URL. This is a deliberate choice: no ingress controller needed, no public endpoints exposed, no certificates to manage.&lt;/p>
&lt;p>When a message comes in, the bot forwards it to the Kagent controller using the A2A (Agent-to-Agent) protocol over HTTP:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AsyncClient&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">KAGENT_BASE_URL&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/api/a2a/kagent/&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">AGENT_NAME&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">payload&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The agent processes the request — which might involve reading docs from GitHub, drafting changes, and preparing a PR — then returns a response. But here&amp;rsquo;s the critical part: &lt;strong>mutating operations require human approval.&lt;/strong>&lt;/p>
&lt;p>When the agent wants to run &lt;code>create_or_update_file&lt;/code>, &lt;code>push_files&lt;/code>, &lt;code>create_pull_request&lt;/code>, or &lt;code>merge_pull_request&lt;/code>, the Slack bot renders an approval prompt:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Tool: create_pull_request&lt;/strong>
Repository: my-org/website
Title: &amp;ldquo;Update getting started guide with new evaluator type&amp;rdquo;
Branch: docs/update-evaluators&lt;/p>
&lt;p>[ Approve ] [ Deny ]&lt;/p>
&lt;/blockquote>
&lt;p>The user clicks a button in Slack. Only then does the agent execute the action. This isn&amp;rsquo;t just a safety measure — it&amp;rsquo;s a trust-building mechanism. People adopt tools they can control.&lt;/p>
&lt;hr>
&lt;h2 id="secrets-management-the-boring-part-that-matters">Secrets Management: The Boring Part That Matters&lt;/h2>
&lt;p>Every credential in this system flows through a single pipeline:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">HashiCorp Vault → External Secrets Operator → Kubernetes Secret → Application
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Vault stores the truth:&lt;/p>
&lt;ul>
&lt;li>LLM API keys at &lt;code>secret/kagent/llm&lt;/code>&lt;/li>
&lt;li>Slack bot tokens at &lt;code>secret/slack&lt;/code>&lt;/li>
&lt;li>GitHub PAT at &lt;code>secret/github&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>External Secrets Operator polls Vault every hour and syncs secrets into Kubernetes. Applications reference Kubernetes secrets in their manifests. Nobody hardcodes a token. Nobody copy-pastes a key into a YAML file.&lt;/p>
&lt;p>The bootstrap script initializes everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Initialize Vault, configure K8s auth, create policies&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./bootstrap/vault-init.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It prompts for each secret interactively, stores them in Vault, and configures the Kubernetes auth method so ESO can retrieve them. One script, one time, all secrets in the right place.&lt;/p>
&lt;hr>
&lt;h2 id="deployment-gitops-all-the-way-down">Deployment: GitOps All the Way Down&lt;/h2>
&lt;p>The entire stack deploys through Argo CD using an App of Apps pattern with sync waves:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Wave&lt;/th>
&lt;th>What Deploys&lt;/th>
&lt;th>Why This Order&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0&lt;/td>
&lt;td>Vault, External Secrets Operator&lt;/td>
&lt;td>Secrets infrastructure first&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1-3&lt;/td>
&lt;td>Istio (ambient mesh)&lt;/td>
&lt;td>Zero-trust networking&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Kagent CRDs&lt;/td>
&lt;td>Agent framework schema&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>Kagent controller&lt;/td>
&lt;td>Agent runtime&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>Vault config (SecretStores, ExternalSecrets)&lt;/td>
&lt;td>Wire secrets to apps&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>Slack bot, GitHub agent&lt;/td>
&lt;td>Application layer&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>One &lt;code>git push&lt;/code> to main and Argo CD reconciles everything. Auto-prune removes deleted resources. Auto-heal fixes drift. If someone &lt;code>kubectl delete&lt;/code>s a resource, Argo puts it back.&lt;/p>
&lt;p>The bootstrap is two commands:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./bootstrap/install.sh &lt;span class="c1"># Argo CD + Gateway API CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./bootstrap/vault-init.sh &lt;span class="c1"># Secrets initialization&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After that, git is the only interface.&lt;/p>
&lt;hr>
&lt;h2 id="what-a-real-interaction-looks-like">What a Real Interaction Looks Like&lt;/h2>
&lt;p>Here&amp;rsquo;s a realistic conversation in Slack:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> The docs for the scoring evaluator are missing the threshold parameter. Can you add it to the reference page?&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> I&amp;rsquo;ll check the current docs. Let me read the evaluator reference page.&lt;/p>
&lt;p>&lt;em>(Agent calls &lt;code>get_file_contents&lt;/code> on the docs repo)&lt;/em>&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> Found it. The scoring evaluator section at &lt;code>content/docs/evaluators/scoring.md&lt;/code> doesn&amp;rsquo;t mention the &lt;code>threshold&lt;/code> parameter. I&amp;rsquo;ll create a branch and update the docs.&lt;/p>
&lt;p>&lt;em>(Agent calls &lt;code>create_branch&lt;/code> → &lt;code>create_or_update_file&lt;/code>)&lt;/em>&lt;/p>
&lt;p>&lt;strong>⚠️ Approval Required&lt;/strong>
&lt;strong>Tool:&lt;/strong> create_or_update_file
&lt;strong>File:&lt;/strong> content/docs/evaluators/scoring.md
&lt;strong>Branch:&lt;/strong> docs/add-threshold-param&lt;/p>
&lt;p>[ Approve ] [ Deny ]&lt;/p>
&lt;p>&lt;strong>You:&lt;/strong> &lt;em>clicks Approve&lt;/em>&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> Done. I&amp;rsquo;ve also opened a PR for review.&lt;/p>
&lt;p>&lt;strong>⚠️ Approval Required&lt;/strong>
&lt;strong>Tool:&lt;/strong> create_pull_request
&lt;strong>Title:&lt;/strong> &amp;ldquo;Add threshold parameter to scoring evaluator docs&amp;rdquo;&lt;/p>
&lt;p>[ Approve ] [ Deny ]&lt;/p>
&lt;p>&lt;strong>You:&lt;/strong> &lt;em>clicks Approve&lt;/em>&lt;/p>
&lt;p>&lt;strong>Agent:&lt;/strong> PR #42 is open. Here&amp;rsquo;s the link: [PR #42]&lt;/p>
&lt;/blockquote>
&lt;p>Total time: under a minute. No browser tabs opened. No git commands typed. No context lost.&lt;/p>
&lt;hr>
&lt;h2 id="lessons-learned">Lessons Learned&lt;/h2>
&lt;p>&lt;strong>Socket Mode is underrated.&lt;/strong> Not needing ingress for a Slack bot simplifies everything. No TLS certs, no DNS records, no service exposure. The bot dials out; Slack sends messages over the websocket. For internal tools, this is the way.&lt;/p>
&lt;p>&lt;strong>MCP is the right abstraction for agent tools.&lt;/strong> Before MCP, every AI agent had bespoke tool integrations — custom functions wrapping API calls. MCP standardizes this. Swap GitHub for GitLab? Replace one MCP server, the agent doesn&amp;rsquo;t change. The protocol decouples the agent from the tools.&lt;/p>
&lt;p>&lt;strong>Human-in-the-loop needs good UX.&lt;/strong> A wall of JSON asking &amp;ldquo;approve this?&amp;rdquo; doesn&amp;rsquo;t cut it. The approval prompt needs to clearly show what action is being taken, on what resource, with what parameters. Slack&amp;rsquo;s interactive blocks make this possible — buttons, structured messages, and threaded conversations.&lt;/p>
&lt;p>&lt;strong>GitOps + AI agents work well together.&lt;/strong> The agent configurations are YAML in git. The secrets pipeline is declarative. The deployment order is encoded in sync waves. When something breaks, &lt;code>git log&lt;/code> tells you what changed. When you want to add a new agent, you add manifests and push. The operational model is the same as any other Kubernetes workload.&lt;/p>
&lt;p>&lt;strong>Start with read-only, add writes carefully.&lt;/strong> The first version of this agent could only read docs and post to Slack. Writes came later, gated behind approvals. This incremental approach builds confidence — both in the system and in the team using it.&lt;/p>
&lt;hr>
&lt;h2 id="try-it-yourself">Try It Yourself&lt;/h2>
&lt;p>The building blocks are all open source:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://github.com/kagent-dev/kagent">Kagent&lt;/a>&lt;/strong> — Kubernetes-native AI agent framework&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://api.githubcopilot.com/mcp/">GitHub MCP Server&lt;/a>&lt;/strong> — GitHub&amp;rsquo;s hosted MCP endpoint (requires a GitHub PAT)&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://slack.dev/bolt-python/">Slack Bolt&lt;/a>&lt;/strong> — Python framework for Slack apps with Socket Mode support&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://external-secrets.io/">External Secrets Operator&lt;/a>&lt;/strong> — Sync secrets from Vault (or other providers) to Kubernetes&lt;/li>
&lt;/ul>
&lt;p>The pattern generalizes beyond documentation. Any workflow that involves reading from a system, deciding on an action, getting human approval, and executing — that&amp;rsquo;s an agent use case. Incident response, infrastructure changes, onboarding checklists. The Slack bot is the interface. Kagent is the brain. MCP servers are the hands.&lt;/p>
&lt;p>The gap between &amp;ldquo;we should update the docs&amp;rdquo; and &amp;ldquo;the docs are updated&amp;rdquo; just got a lot smaller.&lt;/p></content:encoded></item><item><title>Managing FortiGate Firewalls from Telegram with AI, MCP, and kagent</title><link>https://maniak.io/articles/2026-03-15-fortigate-firewall-telegram-kagent-mcp/</link><pubDate>Sun, 15 Mar 2026 10:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-15-fortigate-firewall-telegram-kagent-mcp/</guid><description>&lt;h1 id="managing-fortigate-firewalls-from-telegram-with-ai-mcp-and-kagent">Managing FortiGate Firewalls from Telegram with AI, MCP, and kagent&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>What if you could query your FortiGate firewall from Telegram? Not by logging into the web UI or running CLI commands over SSH — but by asking an AI agent &amp;ldquo;show me all firewall policies&amp;rdquo; or &amp;ldquo;what VIPs are configured?&amp;rdquo; and getting a clean, summarized answer in seconds.&lt;/p>
&lt;p>That&amp;rsquo;s what I built. After wrapping my &lt;a href="https://maniak.io/articles/2026-03-14-f5-bigip-mcp-server-telegram-kagent/">F5 BIG-IP as an MCP server&lt;/a> and managing it from Telegram, I wanted the same experience for my Fortinet firewall. In this article, I&amp;rsquo;ll walk through how I deployed the &lt;a href="https://github.com/alpadalar/fortigate-mcp-server">community FortiGate MCP server&lt;/a> on Kubernetes, wired it up as a &lt;strong>kagent AI agent&lt;/strong>, and connected it to a &lt;strong>Telegram bot&lt;/strong> — giving me full conversational access to firewall policies, NAT rules, VIPs, address objects, routing tables, and system status from my phone.&lt;/p>
&lt;p>Everything runs on my home lab Kubernetes cluster (Talos Linux on Proxmox), deployed via GitOps with ArgoCD, with secrets managed by HashiCorp Vault.&lt;/p>
&lt;h2 id="why-wrap-a-firewall-as-an-mcp-server">Why Wrap a Firewall as an MCP Server?&lt;/h2>
&lt;p>FortiGate has a comprehensive REST API (&lt;code>/api/v2/cmdb/...&lt;/code> and &lt;code>/api/v2/monitor/...&lt;/code>), but it returns deeply nested JSON that&amp;rsquo;s hard to parse at a glance. When you&amp;rsquo;re on your phone and want a quick answer — &amp;ldquo;which policies allow traffic from the DMZ?&amp;rdquo; or &amp;ldquo;what&amp;rsquo;s my NAT setup?&amp;rdquo; — you don&amp;rsquo;t want to stare at raw JSON.&lt;/p>
&lt;p>By wrapping FortiGate as an MCP server and letting an AI agent interpret the results, you get:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Natural language queries&lt;/strong> — ask questions in plain English, get summarized answers&lt;/li>
&lt;li>&lt;strong>Automatic tool discovery&lt;/strong> — kagent discovers all available FortiGate operations via MCP, no manual wiring&lt;/li>
&lt;li>&lt;strong>HITL safety&lt;/strong> — destructive operations (creating policies, modifying routes) require your explicit approval via Telegram buttons&lt;/li>
&lt;li>&lt;strong>Conversational context&lt;/strong> — follow-up questions work naturally (&amp;ldquo;show me policy 5&amp;rdquo; after listing all policies)&lt;/li>
&lt;/ol>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────┬───────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">polling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span> &lt;span class="n">A2A&lt;/span> &lt;span class="n">protocol&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">──────────────────────▶│&lt;/span> &lt;span class="n">kagent&lt;/span> &lt;span class="n">Controller&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">K8s&lt;/span> &lt;span class="n">pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────────────┘&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">routes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">fortigate&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">LLM&lt;/span> &lt;span class="n">decides&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">which&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">streamable&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">HTTP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">FortiGate&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">Server&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">FortiGate&lt;/span> &lt;span class="n">REST&lt;/span> &lt;span class="n">API&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">FortiGate&lt;/span> &lt;span class="n">FW&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Four components, each running as a separate pod in the &lt;code>kagent&lt;/code> namespace:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Telegram Bot&lt;/strong> — polls for messages, sends them to kagent via A2A, renders HITL approval buttons&lt;/li>
&lt;li>&lt;strong>kagent Controller&lt;/strong> — routes the request to the correct agent, manages conversation state&lt;/li>
&lt;li>&lt;strong>fortigate-agent&lt;/strong> — the AI agent (LLM) that decides which MCP tools to call based on your question&lt;/li>
&lt;li>&lt;strong>FortiGate MCP Server&lt;/strong> — translates MCP tool calls into FortiGate REST API calls&lt;/li>
&lt;/ul>
&lt;h2 id="part-1-the-fortigate-mcp-server">Part 1: The FortiGate MCP Server&lt;/h2>
&lt;p>Rather than writing a custom wrapper from scratch, I used the excellent &lt;a href="https://github.com/alpadalar/fortigate-mcp-server">fortigate-mcp-server&lt;/a> community project. It&amp;rsquo;s a well-structured Python application built on FastMCP that exposes 28+ tools across six categories.&lt;/p>
&lt;h3 id="tool-inventory">Tool Inventory&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Tools&lt;/th>
&lt;th>What They Do&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Device&lt;/strong>&lt;/td>
&lt;td>6&lt;/td>
&lt;td>List devices, test connectivity, get system status, discover VDOMs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Firewall&lt;/strong>&lt;/td>
&lt;td>5&lt;/td>
&lt;td>List, create, update, delete policies; get policy details&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Network&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>List/create address objects and service objects&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Routing&lt;/strong>&lt;/td>
&lt;td>7&lt;/td>
&lt;td>Static routes, routing table, interfaces, interface status&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Virtual IPs&lt;/strong>&lt;/td>
&lt;td>5&lt;/td>
&lt;td>List, create, update, delete VIPs; get VIP details&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>System&lt;/strong>&lt;/td>
&lt;td>2+&lt;/td>
&lt;td>Health checks, connection tests, schema info&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="multi-device-support">Multi-Device Support&lt;/h3>
&lt;p>One of the nice features of this MCP server is multi-device support. Each tool takes a &lt;code>device_id&lt;/code> parameter, so you can manage multiple FortiGate appliances from a single server instance. The configuration file defines your device inventory:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;fortigate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;devices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;primary&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;host&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;172.16.10.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">443&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;api_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;your-api-token&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;vdom&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;root&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;verify_ssl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;timeout&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="authentication">Authentication&lt;/h3>
&lt;p>FortiGate supports two auth methods — API tokens and username/password. I use API tokens because they don&amp;rsquo;t expire (unless revoked) and avoid the session management complexity. You generate one in the FortiGate GUI under &lt;strong>System &amp;gt; Administrators &amp;gt; REST API Admin&lt;/strong>.&lt;/p>
&lt;h3 id="containerization">Containerization&lt;/h3>
&lt;p>I built a custom Docker image that generates the &lt;code>config.json&lt;/code> from environment variables at startup. This lets Kubernetes secrets flow cleanly into the container without baking credentials into the image:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>cat &amp;gt; /app/config/config.json &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;fortigate&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;devices&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;primary&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;host&amp;#34;: &amp;#34;${FORTI_HOST}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;port&amp;#34;: ${FORTI_PORT:-443},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;api_token&amp;#34;: &amp;#34;${FORTI_TOKEN}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;vdom&amp;#34;: &amp;#34;${FORTI_VDOM:-root}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;verify_ssl&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;timeout&amp;#34;: 30
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> python -m src.fortigate_mcp.server_http &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --host 0.0.0.0 --port &lt;span class="m">8080&lt;/span> --path /mcp &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --config /app/config/config.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="mcp-transport-streamable-http">MCP Transport: Streamable HTTP&lt;/h3>
&lt;p>A key detail — kagent uses the &lt;strong>Streamable HTTP&lt;/strong> MCP transport, not the older SSE transport. The server needs to call &lt;code>mcp.streamable_http_app()&lt;/code> (not &lt;code>mcp.sse_app()&lt;/code>) and initialize the MCP task group via a Starlette lifespan handler:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mcp_app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mcp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">streamable_http_app&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@asynccontextmanager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">lifespan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">mcp_app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">router&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lifespan_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Starlette&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lifespan&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">lifespan&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">routes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/health&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">health_endpoint&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/mcp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This exposes the MCP endpoint at &lt;code>/mcp/mcp&lt;/code> — kagent POSTs JSON-RPC requests to this URL to discover and invoke tools.&lt;/p>
&lt;h2 id="part-2-the-kagent-agent">Part 2: The kagent Agent&lt;/h2>
&lt;p>The agent is defined as a Kubernetes CRD. It tells kagent what the agent can do, which MCP tools it has access to, and which operations need human approval.&lt;/p>
&lt;h3 id="agent-crd">Agent CRD&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AI agent for managing FortiGate firewall&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are fortigate-agent, an expert AI assistant
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> for managing a FortiGate firewall.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> All tools require a device_id parameter.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> The configured device is called &amp;#34;primary&amp;#34;.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Always pass device_id=&amp;#34;primary&amp;#34; when calling any tool.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Behavioral Guidelines
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1. Confirm destructive operations before executing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2. Present data clearly with tables
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3. Flag overly permissive policies (all/all/accept)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 4. Explain FortiGate API errors in plain language&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="hitl-approval-for-destructive-operations">HITL Approval for Destructive Operations&lt;/h3>
&lt;p>Read operations (listing policies, viewing routes) execute immediately. But anything that modifies the firewall requires your explicit approval:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_firewall_policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">update_firewall_policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_firewall_policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_address_object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_service_object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_static_route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">update_static_route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_static_route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_virtual_ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">update_virtual_ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_virtual_ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When the LLM decides to call one of these tools, kagent pauses execution and sends an approval request back through A2A. The Telegram bot renders this as inline Approve/Reject buttons — you tap to decide.&lt;/p>
&lt;h3 id="mcp-tool-discovery">MCP Tool Discovery&lt;/h3>
&lt;p>The &lt;code>RemoteMCPServer&lt;/code> CRD points kagent at the FortiGate MCP server&amp;rsquo;s endpoint:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://fortigate-mcp-server.kagent:8080/mcp/mcp&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;30s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sseReadTimeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5m0s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>kagent automatically connects, discovers all available tools, and makes them available to the agent. No manual tool registration needed.&lt;/p>
&lt;h2 id="part-3-the-telegram-bot">Part 3: The Telegram Bot&lt;/h2>
&lt;p>The Telegram bot is the same Python bot I use for all my kagent agents. It reuses the &lt;code>sebbycorp/telegram-kagent-bot:latest&lt;/code> image — the only thing that changes between agents is the &lt;code>KAGENT_A2A_URL&lt;/code> environment variable:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-forti-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">KAGENT_A2A_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/fortigate-agent/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Same image, different agent. The bot handles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Message routing&lt;/strong> — sends user messages to the FortiGate agent via A2A&lt;/li>
&lt;li>&lt;strong>Response rendering&lt;/strong> — formats the agent&amp;rsquo;s response (tables, summaries) for Telegram&amp;rsquo;s 4000-char message limit&lt;/li>
&lt;li>&lt;strong>HITL approval&lt;/strong> — renders Approve/Reject inline buttons when the agent needs permission for a destructive operation&lt;/li>
&lt;li>&lt;strong>Conversation context&lt;/strong> — maintains per-user context IDs so follow-up questions work naturally&lt;/li>
&lt;/ul>
&lt;p>The deployment uses &lt;code>strategy: Recreate&lt;/code> because Telegram only allows one poller per bot token.&lt;/p>
&lt;h2 id="part-4-kubernetes-deployment">Part 4: Kubernetes Deployment&lt;/h2>
&lt;p>The entire stack is deployed via GitOps. The manifests live in the &lt;code>k8s-iceman&lt;/code> repo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">manifests/kagent-examples/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── fortigate-agent/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 01-external-secret.yaml # FortiGate creds from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 02-agent.yaml # kagent Agent CRD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 03-deployment.yaml # Deployment + Service + MCP + NetworkPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── telegram-forti-bot/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── 01-external-secret.yaml # Telegram token from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── 02-deployment.yaml # Telegram bot Deployment
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ArgoCD watches this directory and auto-syncs any changes. Push to &lt;code>main&lt;/code> and the stack deploys itself.&lt;/p>
&lt;h3 id="secrets-from-vault">Secrets from Vault&lt;/h3>
&lt;p>FortiGate credentials are stored in HashiCorp Vault and synced to Kubernetes via External Secrets Operator:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FORTI_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">forti_token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FORTI_HOST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Telegram bot token comes from a separate Vault path:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/telegram&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortiaikagentbot_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="network-policy">Network Policy&lt;/h3>
&lt;p>The MCP server is locked down — only pods in the kagent namespace can reach it, and it can only talk to the FortiGate management IP and DNS:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp-server-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ipBlock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">172.16.0.0&lt;/span>&lt;span class="l">/12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UDP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="what-it-looks-like-in-practice">What It Looks Like in Practice&lt;/h2>
&lt;p>Here are some example conversations:&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Show me all firewall policies&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Returns a table with policy ID, name, source/destination interfaces, addresses, services, action, and NAT status.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;What VIPs are configured?&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Lists all Virtual IP mappings with external IP, mapped IP, port forwarding settings, and associated interfaces.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Show me the routing table&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Displays the active routing table with destinations, gateways, interfaces, and metrics.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Create an address object called test-server with IP 10.0.5.100/32&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;I&amp;rsquo;ll create an address object &amp;rsquo;test-server&amp;rsquo; with type &amp;lsquo;ipmask&amp;rsquo; and address &amp;lsquo;10.0.5.100/32&amp;rsquo;.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Telegram shows&lt;/strong>: &lt;code>[Approve] [Reject]&lt;/code> buttons&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: Tap &lt;code>Approve&lt;/code>&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;Address object &amp;rsquo;test-server&amp;rsquo; created successfully.&amp;rdquo;&lt;/p>
&lt;h2 id="safety-guardrails">Safety Guardrails&lt;/h2>
&lt;p>This is network security infrastructure — the safety layers matter:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Guardrail&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent&lt;/strong>&lt;/td>
&lt;td>System prompt instructs the LLM to confirm destructive ops, flag overly permissive policies&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong>&lt;/td>
&lt;td>&lt;code>requireApproval&lt;/code> on 11 write operations — execution pauses until human approves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Telegram&lt;/strong>&lt;/td>
&lt;td>HITL approval rendered as inline Approve/Reject buttons&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FortiGate&lt;/strong>&lt;/td>
&lt;td>API token scoped to specific admin profile with limited permissions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Network&lt;/strong>&lt;/td>
&lt;td>NetworkPolicy restricts who can reach the MCP server and where it can connect&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="lessons-learned">Lessons Learned&lt;/h2>
&lt;p>A few things I ran into while building this:&lt;/p>
&lt;p>&lt;strong>MCP transport matters.&lt;/strong> kagent uses Streamable HTTP, not SSE. If your MCP server only serves the older SSE transport, kagent will fail with &amp;ldquo;Method Not Allowed&amp;rdquo; on POST. Use &lt;code>mcp.streamable_http_app()&lt;/code> and initialize the task group via a Starlette lifespan.&lt;/p>
&lt;p>&lt;strong>Lifespan initialization is required.&lt;/strong> The Streamable HTTP app needs &lt;code>async with mcp_app.router.lifespan_context(mcp_app)&lt;/code> in your Starlette lifespan handler. Without it, you get &amp;ldquo;Task group is not initialized&amp;rdquo; errors.&lt;/p>
&lt;p>&lt;strong>Device ID is a required parameter.&lt;/strong> Every tool in the community MCP server requires a &lt;code>device_id&lt;/code>. Make sure your agent&amp;rsquo;s system prompt tells the LLM to always pass &lt;code>device_id=&amp;quot;primary&amp;quot;&lt;/code> (or whatever you named your device in the config).&lt;/p>
&lt;p>&lt;strong>Vault key paths depend on the ClusterSecretStore.&lt;/strong> If your ESO ClusterSecretStore has &lt;code>path: &amp;quot;secret&amp;quot;&lt;/code> configured, the ExternalSecret key should be just &lt;code>fortigate&lt;/code>, not &lt;code>secret/fortigate&lt;/code> — the provider prepends the path automatically.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>This project reinforces the same patterns I&amp;rsquo;ve been exploring with &lt;a href="https://maniak.io/articles/2026-03-14-f5-bigip-mcp-server-telegram-kagent/">F5 BIG-IP&lt;/a>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP as the universal integration layer&lt;/strong> — wrap any API as an MCP server and let AI agents discover and use it&lt;/li>
&lt;li>&lt;strong>kagent for agent lifecycle on Kubernetes&lt;/strong> — define agents as CRDs, deploy with GitOps, get HITL approval for free&lt;/li>
&lt;li>&lt;strong>A2A for agent communication&lt;/strong> — the Telegram bot is agent-agnostic; it speaks A2A to kagent, which routes to the right agent&lt;/li>
&lt;li>&lt;strong>Reusable bot pattern&lt;/strong> — same Telegram bot image, different &lt;code>KAGENT_A2A_URL&lt;/code>, instant new agent interface&lt;/li>
&lt;/ul>
&lt;p>The FortiGate MCP server gives you 28+ tools covering device management, firewall policies, network objects, routing, VIPs, and system monitoring. And because it&amp;rsquo;s MCP, adding more tools is just adding more functions — kagent discovers them automatically.&lt;/p>
&lt;p>The full source code is available in the &lt;a href="https://github.com/ProfessorSeb/k8s-iceman">k8s-iceman&lt;/a> repository:&lt;/p>
&lt;ul>
&lt;li>FortiGate MCP server modifications: &lt;code>apps/fortigate-wrapper-src/&lt;/code>&lt;/li>
&lt;li>Agent manifests: &lt;code>manifests/kagent-examples/fortigate-agent/&lt;/code>&lt;/li>
&lt;li>Telegram bot manifests: &lt;code>manifests/kagent-examples/telegram-forti-bot/&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h1 id="managing-fortigate-firewalls-from-telegram-with-ai-mcp-and-kagent">Managing FortiGate Firewalls from Telegram with AI, MCP, and kagent&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>What if you could query your FortiGate firewall from Telegram? Not by logging into the web UI or running CLI commands over SSH — but by asking an AI agent &amp;ldquo;show me all firewall policies&amp;rdquo; or &amp;ldquo;what VIPs are configured?&amp;rdquo; and getting a clean, summarized answer in seconds.&lt;/p>
&lt;p>That&amp;rsquo;s what I built. After wrapping my &lt;a href="https://maniak.io/articles/2026-03-14-f5-bigip-mcp-server-telegram-kagent/">F5 BIG-IP as an MCP server&lt;/a> and managing it from Telegram, I wanted the same experience for my Fortinet firewall. In this article, I&amp;rsquo;ll walk through how I deployed the &lt;a href="https://github.com/alpadalar/fortigate-mcp-server">community FortiGate MCP server&lt;/a> on Kubernetes, wired it up as a &lt;strong>kagent AI agent&lt;/strong>, and connected it to a &lt;strong>Telegram bot&lt;/strong> — giving me full conversational access to firewall policies, NAT rules, VIPs, address objects, routing tables, and system status from my phone.&lt;/p>
&lt;p>Everything runs on my home lab Kubernetes cluster (Talos Linux on Proxmox), deployed via GitOps with ArgoCD, with secrets managed by HashiCorp Vault.&lt;/p>
&lt;h2 id="why-wrap-a-firewall-as-an-mcp-server">Why Wrap a Firewall as an MCP Server?&lt;/h2>
&lt;p>FortiGate has a comprehensive REST API (&lt;code>/api/v2/cmdb/...&lt;/code> and &lt;code>/api/v2/monitor/...&lt;/code>), but it returns deeply nested JSON that&amp;rsquo;s hard to parse at a glance. When you&amp;rsquo;re on your phone and want a quick answer — &amp;ldquo;which policies allow traffic from the DMZ?&amp;rdquo; or &amp;ldquo;what&amp;rsquo;s my NAT setup?&amp;rdquo; — you don&amp;rsquo;t want to stare at raw JSON.&lt;/p>
&lt;p>By wrapping FortiGate as an MCP server and letting an AI agent interpret the results, you get:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Natural language queries&lt;/strong> — ask questions in plain English, get summarized answers&lt;/li>
&lt;li>&lt;strong>Automatic tool discovery&lt;/strong> — kagent discovers all available FortiGate operations via MCP, no manual wiring&lt;/li>
&lt;li>&lt;strong>HITL safety&lt;/strong> — destructive operations (creating policies, modifying routes) require your explicit approval via Telegram buttons&lt;/li>
&lt;li>&lt;strong>Conversational context&lt;/strong> — follow-up questions work naturally (&amp;ldquo;show me policy 5&amp;rdquo; after listing all policies)&lt;/li>
&lt;/ol>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────┬───────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">polling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span> &lt;span class="n">A2A&lt;/span> &lt;span class="n">protocol&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">──────────────────────▶│&lt;/span> &lt;span class="n">kagent&lt;/span> &lt;span class="n">Controller&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">K8s&lt;/span> &lt;span class="n">pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────────────┘&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">routes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">fortigate&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">LLM&lt;/span> &lt;span class="n">decides&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">which&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">streamable&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">HTTP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">FortiGate&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">Server&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">FortiGate&lt;/span> &lt;span class="n">REST&lt;/span> &lt;span class="n">API&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">FortiGate&lt;/span> &lt;span class="n">FW&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Four components, each running as a separate pod in the &lt;code>kagent&lt;/code> namespace:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Telegram Bot&lt;/strong> — polls for messages, sends them to kagent via A2A, renders HITL approval buttons&lt;/li>
&lt;li>&lt;strong>kagent Controller&lt;/strong> — routes the request to the correct agent, manages conversation state&lt;/li>
&lt;li>&lt;strong>fortigate-agent&lt;/strong> — the AI agent (LLM) that decides which MCP tools to call based on your question&lt;/li>
&lt;li>&lt;strong>FortiGate MCP Server&lt;/strong> — translates MCP tool calls into FortiGate REST API calls&lt;/li>
&lt;/ul>
&lt;h2 id="part-1-the-fortigate-mcp-server">Part 1: The FortiGate MCP Server&lt;/h2>
&lt;p>Rather than writing a custom wrapper from scratch, I used the excellent &lt;a href="https://github.com/alpadalar/fortigate-mcp-server">fortigate-mcp-server&lt;/a> community project. It&amp;rsquo;s a well-structured Python application built on FastMCP that exposes 28+ tools across six categories.&lt;/p>
&lt;h3 id="tool-inventory">Tool Inventory&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Tools&lt;/th>
&lt;th>What They Do&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Device&lt;/strong>&lt;/td>
&lt;td>6&lt;/td>
&lt;td>List devices, test connectivity, get system status, discover VDOMs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Firewall&lt;/strong>&lt;/td>
&lt;td>5&lt;/td>
&lt;td>List, create, update, delete policies; get policy details&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Network&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>List/create address objects and service objects&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Routing&lt;/strong>&lt;/td>
&lt;td>7&lt;/td>
&lt;td>Static routes, routing table, interfaces, interface status&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Virtual IPs&lt;/strong>&lt;/td>
&lt;td>5&lt;/td>
&lt;td>List, create, update, delete VIPs; get VIP details&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>System&lt;/strong>&lt;/td>
&lt;td>2+&lt;/td>
&lt;td>Health checks, connection tests, schema info&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="multi-device-support">Multi-Device Support&lt;/h3>
&lt;p>One of the nice features of this MCP server is multi-device support. Each tool takes a &lt;code>device_id&lt;/code> parameter, so you can manage multiple FortiGate appliances from a single server instance. The configuration file defines your device inventory:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;fortigate&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;devices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;primary&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;host&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;172.16.10.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">443&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;api_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;your-api-token&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;vdom&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;root&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;verify_ssl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;timeout&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">30&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="authentication">Authentication&lt;/h3>
&lt;p>FortiGate supports two auth methods — API tokens and username/password. I use API tokens because they don&amp;rsquo;t expire (unless revoked) and avoid the session management complexity. You generate one in the FortiGate GUI under &lt;strong>System &amp;gt; Administrators &amp;gt; REST API Admin&lt;/strong>.&lt;/p>
&lt;h3 id="containerization">Containerization&lt;/h3>
&lt;p>I built a custom Docker image that generates the &lt;code>config.json&lt;/code> from environment variables at startup. This lets Kubernetes secrets flow cleanly into the container without baking credentials into the image:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>cat &amp;gt; /app/config/config.json &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;fortigate&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;devices&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;primary&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;host&amp;#34;: &amp;#34;${FORTI_HOST}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;port&amp;#34;: ${FORTI_PORT:-443},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;api_token&amp;#34;: &amp;#34;${FORTI_TOKEN}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;vdom&amp;#34;: &amp;#34;${FORTI_VDOM:-root}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;verify_ssl&amp;#34;: false,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;timeout&amp;#34;: 30
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">exec&lt;/span> python -m src.fortigate_mcp.server_http &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --host 0.0.0.0 --port &lt;span class="m">8080&lt;/span> --path /mcp &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --config /app/config/config.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="mcp-transport-streamable-http">MCP Transport: Streamable HTTP&lt;/h3>
&lt;p>A key detail — kagent uses the &lt;strong>Streamable HTTP&lt;/strong> MCP transport, not the older SSE transport. The server needs to call &lt;code>mcp.streamable_http_app()&lt;/code> (not &lt;code>mcp.sse_app()&lt;/code>) and initialize the MCP task group via a Starlette lifespan handler:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mcp_app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mcp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">streamable_http_app&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@asynccontextmanager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">lifespan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">mcp_app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">router&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lifespan_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Starlette&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lifespan&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">lifespan&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">routes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/health&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">health_endpoint&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/mcp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This exposes the MCP endpoint at &lt;code>/mcp/mcp&lt;/code> — kagent POSTs JSON-RPC requests to this URL to discover and invoke tools.&lt;/p>
&lt;h2 id="part-2-the-kagent-agent">Part 2: The kagent Agent&lt;/h2>
&lt;p>The agent is defined as a Kubernetes CRD. It tells kagent what the agent can do, which MCP tools it has access to, and which operations need human approval.&lt;/p>
&lt;h3 id="agent-crd">Agent CRD&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AI agent for managing FortiGate firewall&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are fortigate-agent, an expert AI assistant
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> for managing a FortiGate firewall.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> All tools require a device_id parameter.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> The configured device is called &amp;#34;primary&amp;#34;.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Always pass device_id=&amp;#34;primary&amp;#34; when calling any tool.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Behavioral Guidelines
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1. Confirm destructive operations before executing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2. Present data clearly with tables
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3. Flag overly permissive policies (all/all/accept)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 4. Explain FortiGate API errors in plain language&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="hitl-approval-for-destructive-operations">HITL Approval for Destructive Operations&lt;/h3>
&lt;p>Read operations (listing policies, viewing routes) execute immediately. But anything that modifies the firewall requires your explicit approval:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_firewall_policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">update_firewall_policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_firewall_policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_address_object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_service_object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_static_route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">update_static_route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_static_route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_virtual_ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">update_virtual_ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_virtual_ip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When the LLM decides to call one of these tools, kagent pauses execution and sends an approval request back through A2A. The Telegram bot renders this as inline Approve/Reject buttons — you tap to decide.&lt;/p>
&lt;h3 id="mcp-tool-discovery">MCP Tool Discovery&lt;/h3>
&lt;p>The &lt;code>RemoteMCPServer&lt;/code> CRD points kagent at the FortiGate MCP server&amp;rsquo;s endpoint:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://fortigate-mcp-server.kagent:8080/mcp/mcp&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;30s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sseReadTimeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;5m0s&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>kagent automatically connects, discovers all available tools, and makes them available to the agent. No manual tool registration needed.&lt;/p>
&lt;h2 id="part-3-the-telegram-bot">Part 3: The Telegram Bot&lt;/h2>
&lt;p>The Telegram bot is the same Python bot I use for all my kagent agents. It reuses the &lt;code>sebbycorp/telegram-kagent-bot:latest&lt;/code> image — the only thing that changes between agents is the &lt;code>KAGENT_A2A_URL&lt;/code> environment variable:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-forti-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">KAGENT_A2A_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/fortigate-agent/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Same image, different agent. The bot handles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Message routing&lt;/strong> — sends user messages to the FortiGate agent via A2A&lt;/li>
&lt;li>&lt;strong>Response rendering&lt;/strong> — formats the agent&amp;rsquo;s response (tables, summaries) for Telegram&amp;rsquo;s 4000-char message limit&lt;/li>
&lt;li>&lt;strong>HITL approval&lt;/strong> — renders Approve/Reject inline buttons when the agent needs permission for a destructive operation&lt;/li>
&lt;li>&lt;strong>Conversation context&lt;/strong> — maintains per-user context IDs so follow-up questions work naturally&lt;/li>
&lt;/ul>
&lt;p>The deployment uses &lt;code>strategy: Recreate&lt;/code> because Telegram only allows one poller per bot token.&lt;/p>
&lt;h2 id="part-4-kubernetes-deployment">Part 4: Kubernetes Deployment&lt;/h2>
&lt;p>The entire stack is deployed via GitOps. The manifests live in the &lt;code>k8s-iceman&lt;/code> repo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">manifests/kagent-examples/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── fortigate-agent/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 01-external-secret.yaml # FortiGate creds from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 02-agent.yaml # kagent Agent CRD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 03-deployment.yaml # Deployment + Service + MCP + NetworkPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── telegram-forti-bot/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── 01-external-secret.yaml # Telegram token from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── 02-deployment.yaml # Telegram bot Deployment
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ArgoCD watches this directory and auto-syncs any changes. Push to &lt;code>main&lt;/code> and the stack deploys itself.&lt;/p>
&lt;h3 id="secrets-from-vault">Secrets from Vault&lt;/h3>
&lt;p>FortiGate credentials are stored in HashiCorp Vault and synced to Kubernetes via External Secrets Operator:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FORTI_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">forti_token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">FORTI_HOST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Telegram bot token comes from a separate Vault path:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/telegram&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortiaikagentbot_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="network-policy">Network Policy&lt;/h3>
&lt;p>The MCP server is locked down — only pods in the kagent namespace can reach it, and it can only talk to the FortiGate management IP and DNS:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp-server-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fortigate-mcp-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ipBlock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">172.16.0.0&lt;/span>&lt;span class="l">/12&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UDP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="what-it-looks-like-in-practice">What It Looks Like in Practice&lt;/h2>
&lt;p>Here are some example conversations:&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Show me all firewall policies&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Returns a table with policy ID, name, source/destination interfaces, addresses, services, action, and NAT status.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;What VIPs are configured?&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Lists all Virtual IP mappings with external IP, mapped IP, port forwarding settings, and associated interfaces.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Show me the routing table&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Displays the active routing table with destinations, gateways, interfaces, and metrics.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Create an address object called test-server with IP 10.0.5.100/32&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;I&amp;rsquo;ll create an address object &amp;rsquo;test-server&amp;rsquo; with type &amp;lsquo;ipmask&amp;rsquo; and address &amp;lsquo;10.0.5.100/32&amp;rsquo;.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Telegram shows&lt;/strong>: &lt;code>[Approve] [Reject]&lt;/code> buttons&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: Tap &lt;code>Approve&lt;/code>&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;Address object &amp;rsquo;test-server&amp;rsquo; created successfully.&amp;rdquo;&lt;/p>
&lt;h2 id="safety-guardrails">Safety Guardrails&lt;/h2>
&lt;p>This is network security infrastructure — the safety layers matter:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Guardrail&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent&lt;/strong>&lt;/td>
&lt;td>System prompt instructs the LLM to confirm destructive ops, flag overly permissive policies&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong>&lt;/td>
&lt;td>&lt;code>requireApproval&lt;/code> on 11 write operations — execution pauses until human approves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Telegram&lt;/strong>&lt;/td>
&lt;td>HITL approval rendered as inline Approve/Reject buttons&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>FortiGate&lt;/strong>&lt;/td>
&lt;td>API token scoped to specific admin profile with limited permissions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Network&lt;/strong>&lt;/td>
&lt;td>NetworkPolicy restricts who can reach the MCP server and where it can connect&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="lessons-learned">Lessons Learned&lt;/h2>
&lt;p>A few things I ran into while building this:&lt;/p>
&lt;p>&lt;strong>MCP transport matters.&lt;/strong> kagent uses Streamable HTTP, not SSE. If your MCP server only serves the older SSE transport, kagent will fail with &amp;ldquo;Method Not Allowed&amp;rdquo; on POST. Use &lt;code>mcp.streamable_http_app()&lt;/code> and initialize the task group via a Starlette lifespan.&lt;/p>
&lt;p>&lt;strong>Lifespan initialization is required.&lt;/strong> The Streamable HTTP app needs &lt;code>async with mcp_app.router.lifespan_context(mcp_app)&lt;/code> in your Starlette lifespan handler. Without it, you get &amp;ldquo;Task group is not initialized&amp;rdquo; errors.&lt;/p>
&lt;p>&lt;strong>Device ID is a required parameter.&lt;/strong> Every tool in the community MCP server requires a &lt;code>device_id&lt;/code>. Make sure your agent&amp;rsquo;s system prompt tells the LLM to always pass &lt;code>device_id=&amp;quot;primary&amp;quot;&lt;/code> (or whatever you named your device in the config).&lt;/p>
&lt;p>&lt;strong>Vault key paths depend on the ClusterSecretStore.&lt;/strong> If your ESO ClusterSecretStore has &lt;code>path: &amp;quot;secret&amp;quot;&lt;/code> configured, the ExternalSecret key should be just &lt;code>fortigate&lt;/code>, not &lt;code>secret/fortigate&lt;/code> — the provider prepends the path automatically.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>This project reinforces the same patterns I&amp;rsquo;ve been exploring with &lt;a href="https://maniak.io/articles/2026-03-14-f5-bigip-mcp-server-telegram-kagent/">F5 BIG-IP&lt;/a>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP as the universal integration layer&lt;/strong> — wrap any API as an MCP server and let AI agents discover and use it&lt;/li>
&lt;li>&lt;strong>kagent for agent lifecycle on Kubernetes&lt;/strong> — define agents as CRDs, deploy with GitOps, get HITL approval for free&lt;/li>
&lt;li>&lt;strong>A2A for agent communication&lt;/strong> — the Telegram bot is agent-agnostic; it speaks A2A to kagent, which routes to the right agent&lt;/li>
&lt;li>&lt;strong>Reusable bot pattern&lt;/strong> — same Telegram bot image, different &lt;code>KAGENT_A2A_URL&lt;/code>, instant new agent interface&lt;/li>
&lt;/ul>
&lt;p>The FortiGate MCP server gives you 28+ tools covering device management, firewall policies, network objects, routing, VIPs, and system monitoring. And because it&amp;rsquo;s MCP, adding more tools is just adding more functions — kagent discovers them automatically.&lt;/p>
&lt;p>The full source code is available in the &lt;a href="https://github.com/ProfessorSeb/k8s-iceman">k8s-iceman&lt;/a> repository:&lt;/p>
&lt;ul>
&lt;li>FortiGate MCP server modifications: &lt;code>apps/fortigate-wrapper-src/&lt;/code>&lt;/li>
&lt;li>Agent manifests: &lt;code>manifests/kagent-examples/fortigate-agent/&lt;/code>&lt;/li>
&lt;li>Telegram bot manifests: &lt;code>manifests/kagent-examples/telegram-forti-bot/&lt;/code>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Managing F5 BIG-IP from Telegram with an AI Agent, MCP, and kagent</title><link>https://maniak.io/articles/2026-03-14-f5-bigip-mcp-server-telegram-kagent/</link><pubDate>Sat, 14 Mar 2026 10:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-14-f5-bigip-mcp-server-telegram-kagent/</guid><description>&lt;h1 id="managing-f5-big-ip-from-telegram-with-an-ai-agent-mcp-and-kagent">Managing F5 BIG-IP from Telegram with an AI Agent, MCP, and kagent&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>What if you could manage your F5 BIG-IP load balancer from Telegram? Not by SSH-ing into the box and running &lt;code>tmsh&lt;/code> — but by telling an AI agent in plain English to &amp;ldquo;show me all pools&amp;rdquo; or &amp;ldquo;disable node 10.0.1.25&amp;rdquo; and having it figure out the right iControl REST calls, execute them safely, and ask for your approval before anything destructive happens?&lt;/p>
&lt;p>That&amp;rsquo;s exactly what I built. In this article, I&amp;rsquo;ll walk through the full stack: a custom &lt;strong>MCP (Model Context Protocol) server&lt;/strong> that wraps the F5 iControl REST API, a &lt;strong>kagent AI agent&lt;/strong> on Kubernetes that uses those tools to manage the BIG-IP, and a &lt;strong>Telegram bot&lt;/strong> that lets you talk to the agent from your phone — complete with &lt;strong>Human-in-the-Loop (HITL) approval buttons&lt;/strong> for destructive operations.&lt;/p>
&lt;p>Everything runs on my home lab Kubernetes cluster (Talos Linux on Proxmox), deployed via GitOps with ArgoCD, with secrets managed by HashiCorp Vault.&lt;/p>
&lt;h2 id="why-not-just-use-the-f5-api-directly">Why Not Just Use the F5 API Directly?&lt;/h2>
&lt;p>The F5 iControl REST API is massive. It uses token-based auth with expiring sessions, returns deeply nested JSON, and has hundreds of endpoints. Talking to it directly from an AI agent would be fragile and wasteful.&lt;/p>
&lt;p>I needed a wrapper that:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Exposes only what the agent needs&lt;/strong> — a curated set of pool, virtual server, node, monitor, iRule, certificate, and system operations instead of the full iControl surface area&lt;/li>
&lt;li>&lt;strong>Handles auth in one place&lt;/strong> — token acquisition, automatic refresh before the 1200s expiry, and clean logout on shutdown&lt;/li>
&lt;li>&lt;strong>Speaks MCP natively&lt;/strong> — so kagent discovers all tools automatically via a single endpoint, no manual tool registration needed&lt;/li>
&lt;li>&lt;strong>Adds guardrails&lt;/strong> — read-only mode, partition allow-lists, HITL approval for destructive operations, and clear error responses the agent can reason about&lt;/li>
&lt;/ol>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s how it all fits together:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────┬───────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">polling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span> &lt;span class="n">A2A&lt;/span> &lt;span class="n">protocol&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">──────────────────────▶│&lt;/span> &lt;span class="n">kagent&lt;/span> &lt;span class="n">Controller&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">K8s&lt;/span> &lt;span class="n">pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────────────┘&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">routes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">f5&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bigip&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">LLM&lt;/span> &lt;span class="n">decides&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">which&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">streamable&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">HTTP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">F5&lt;/span> &lt;span class="n">Wrapper&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">FastAPI&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">MCP&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">iControl&lt;/span> &lt;span class="n">REST&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">F5&lt;/span> &lt;span class="n">BIG&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="ne">IP&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Four components, each running as a separate pod in the &lt;code>kagent&lt;/code> namespace:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Telegram Bot&lt;/strong> — polls for messages, sends them to kagent via A2A, renders HITL approval buttons&lt;/li>
&lt;li>&lt;strong>kagent Controller&lt;/strong> — routes the request to the correct agent, manages conversation state&lt;/li>
&lt;li>&lt;strong>f5-bigip-agent&lt;/strong> — the AI agent (LLM) that decides which MCP tools to call based on the user&amp;rsquo;s natural language request&lt;/li>
&lt;li>&lt;strong>F5 Wrapper&lt;/strong> — FastAPI service that translates MCP tool calls into iControl REST API calls against the BIG-IP&lt;/li>
&lt;/ul>
&lt;h2 id="part-1-the-f5-mcp-wrapper">Part 1: The F5 MCP Wrapper&lt;/h2>
&lt;p>The wrapper is a Python FastAPI application that serves two purposes: a REST API for direct HTTP access and an MCP server for kagent tool discovery.&lt;/p>
&lt;h3 id="project-structure">Project Structure&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">f5&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">wrapper&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">├──&lt;/span> &lt;span class="n">Dockerfile&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">├──&lt;/span> &lt;span class="n">requirements&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">txt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">├──&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">main&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># Starlette root — mounts REST + MCP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">mcp_server&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># 28 MCP tool definitions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># Pydantic Settings (env vars)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">auth&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># F5 token manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">routers&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="c1"># REST API endpoints&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">pools&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">virtual_servers&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">nodes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">monitors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">irules&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">certificates&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">└──&lt;/span> &lt;span class="n">system&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└──&lt;/span> &lt;span class="n">utils&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└──&lt;/span> &lt;span class="n">f5_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># Reusable HTTP client for iControl REST&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="authentication-token-lifecycle">Authentication: Token Lifecycle&lt;/h3>
&lt;p>The F5 iControl REST API uses token-based auth. Rather than handling this in every tool, the wrapper manages the entire token lifecycle transparently:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">F5TokenManager&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">host&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">password&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">verify_ssl&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">host&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">host&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_expiry&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">login&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/mgmt/shared/authn/login&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;username&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;password&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">password&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;loginProviderName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tmos&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()[&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Tokens last 1200s; refresh at 80% (960s)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_expiry&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">960&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">get_token&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_expiry&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">login&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">logout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Clean up token on shutdown&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/mgmt/shared/authz/tokens/&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;X-F5-Auth-Token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On startup, the app logs in and stores the token. Every request checks if the token is about to expire and refreshes automatically. On shutdown, the token is deleted.&lt;/p>
&lt;h3 id="mcp-tool-definitions-28-tools">MCP Tool Definitions (28 Tools)&lt;/h3>
&lt;p>The MCP server uses the &lt;code>FastMCP&lt;/code> library to expose tools that kagent discovers automatically. Here&amp;rsquo;s the full tool inventory:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Tools&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Pools&lt;/strong>&lt;/td>
&lt;td>8&lt;/td>
&lt;td>List, create, delete pools; add/remove/enable/disable members&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Virtual Servers&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>List, get details, create, delete virtual servers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Nodes&lt;/strong>&lt;/td>
&lt;td>5&lt;/td>
&lt;td>List, get, create, delete nodes; enable/disable/force-offline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Monitors&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>List all monitors, HTTP, HTTPS, and TCP monitors&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>iRules&lt;/strong>&lt;/td>
&lt;td>2&lt;/td>
&lt;td>List iRules, get full TCL definitions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Certificates&lt;/strong>&lt;/td>
&lt;td>2&lt;/td>
&lt;td>List SSL certs and expiration dates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>System&lt;/strong>&lt;/td>
&lt;td>3&lt;/td>
&lt;td>BIG-IP version/hostname, HA failover status, config sync status&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each tool is a simple async function decorated with &lt;code>@mcp.tool()&lt;/code>. For example, here&amp;rsquo;s the pool listing tool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@mcp.tool&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">list_pools&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">partition&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Common&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;List all LTM pools in the specified partition.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">_client&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;/mgmt/tm/ltm/pool?$filter=partition eq &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">partition&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">items&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;items&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">item&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">items&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;partition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;partition&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Common&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;monitor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;monitor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;lb_method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;loadBalancingMode&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;round-robin&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And a write operation with read-only guard:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@mcp.tool&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_pool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">partition&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Common&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">monitor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/Common/http&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lb_method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;round-robin&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">members&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Create a new LTM pool with optional members.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">READ_ONLY&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;ERROR: Service is in read-only mode&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;partition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">partition&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;monitor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">monitor&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;loadBalancingMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">lb_method&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">members&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;members&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">members&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">members&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="n">members&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">_client&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/mgmt/tm/ltm/pool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">payload&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="mounting-mcp--rest-together">Mounting MCP + REST Together&lt;/h3>
&lt;p>The app uses Starlette as the root application to mount both the MCP server and the FastAPI REST API under different paths:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># MCP streamable-HTTP app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mcp_app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mcp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">streamable_http_app&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Main Starlette app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Starlette&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lifespan&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">lifespan&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">routes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/health&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">health&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/mcp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># MCP tools for kagent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">rest_app&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># REST API + Swagger docs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The lifespan handler initializes the F5 token manager on startup and triggers the MCP sub-app&amp;rsquo;s lifespan to set up its async task group:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@asynccontextmanager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">lifespan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">F5TokenManager&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">F5_HOST&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">tm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">login&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">set_token_manager&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tm&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">mcp_app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">router&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lifespan_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">tm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">logout&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="configuration">Configuration&lt;/h3>
&lt;p>All configuration is via environment variables, loaded by Pydantic Settings:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>Required&lt;/th>
&lt;th>Default&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>F5_HOST&lt;/code>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>—&lt;/td>
&lt;td>BIG-IP management URL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_USERNAME&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>admin&lt;/code>&lt;/td>
&lt;td>iControl REST username&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_PASSWORD&lt;/code>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>—&lt;/td>
&lt;td>iControl REST password&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_VERIFY_SSL&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>Verify F5 SSL certificate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_PARTITION&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>Common&lt;/code>&lt;/td>
&lt;td>Default BIG-IP partition&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ALLOWED_PARTITIONS&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>Common&lt;/code>&lt;/td>
&lt;td>Partition allow-list&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>READ_ONLY&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>Block all write operations&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="part-2-the-kagent-agent">Part 2: The kagent Agent&lt;/h2>
&lt;p>The AI agent is defined as a Kubernetes CRD — a declarative &lt;code>Agent&lt;/code> resource that tells kagent what the agent can do, which tools it has, and which operations need human approval.&lt;/p>
&lt;h3 id="agent-crd">Agent CRD&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AI agent for managing F5 BIG-IP load balancer&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are f5-bigip-agent, an expert AI assistant for managing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> the F5 BIG-IP load balancer.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Behavioral Guidelines
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1. Always confirm destructive operations before executing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2. Check dependencies before deleting (e.g., is a VS using this pool?)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3. Check HA failover status before write operations
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 4. Use tables for listing multiple resources
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ...&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="hitl-approval">HITL Approval&lt;/h3>
&lt;p>The critical piece is &lt;code>requireApproval&lt;/code> — this tells kagent that 10 destructive operations must get human approval before execution:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_pool&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_pool&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">add_pool_member&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">remove_pool_member&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">set_pool_member_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_virtual_server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_virtual_server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">set_node_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When the LLM decides to call one of these tools, kagent pauses execution and sends an approval request back through the A2A protocol. The Telegram bot renders this as Approve/Reject buttons.&lt;/p>
&lt;h3 id="a2a-skills">A2A Skills&lt;/h3>
&lt;p>The agent also advertises its capabilities via A2A skills, so other agents or clients can discover what it does:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">a2aConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">skills&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5 BIG-IP Operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Manage pools, virtual servers, nodes, monitors,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> and system status on F5 BIG-IP&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">examples&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Show me all pools on the F5&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Disable member 10.0.1.25:80 in pool prod-web&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;What is the failover status of the BIG-IP?&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-3-the-telegram-bot">Part 3: The Telegram Bot&lt;/h2>
&lt;p>The Telegram bot is the same Python bot I built for &lt;a href="https://maniak.io/articles/2026-03-13-telegram-bot-kagent-a2a-kubernetes/">general kagent operations&lt;/a>, redeployed with a different environment variable pointing it at the F5 agent:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-f5-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">KAGENT_A2A_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/f5-bigip-agent/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it — same bot image (&lt;code>sebbycorp/telegram-kagent-bot:latest&lt;/code>), different agent URL. The bot polls Telegram for messages, sends them to the F5 agent via A2A, and streams the response back to the chat. When a HITL approval is needed, it shows inline Approve/Reject buttons.&lt;/p>
&lt;p>The deployment uses &lt;code>strategy: Recreate&lt;/code> because Telegram only allows one poller per bot token.&lt;/p>
&lt;h2 id="part-4-kubernetes-deployment">Part 4: Kubernetes Deployment&lt;/h2>
&lt;p>The entire stack is deployed via GitOps. The manifests live in the &lt;code>k8s-iceman&lt;/code> repo under &lt;code>manifests/kagent-examples/&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">manifests/kagent-examples/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── f5-agent/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── bigip/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 01-external-secret.yaml # F5 creds from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 02-agent.yaml # kagent Agent CRD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 03-deployment.yaml # Deployment + Service + MCP + NetworkPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── telegram-f5-bot/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── 01-external-secret.yaml # Telegram token from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── 02-deployment.yaml # Telegram bot Deployment
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ArgoCD watches this directory and auto-syncs any changes.&lt;/p>
&lt;h3 id="secrets-from-vault">Secrets from Vault&lt;/h3>
&lt;p>F5 credentials are stored in HashiCorp Vault and injected via External Secrets Operator:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5_HOST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/f5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5_USERNAME&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/f5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">username&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5_PASSWORD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/f5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="network-policy">Network Policy&lt;/h3>
&lt;p>The wrapper is locked down with a NetworkPolicy that only allows ingress from the kagent namespace and egress to the F5 management IP + DNS:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-wrapper-bigip-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-wrapper-bigip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ipBlock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.0.0.0&lt;/span>&lt;span class="l">/8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UDP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="safety-guardrails">Safety Guardrails&lt;/h2>
&lt;p>This is network infrastructure — you don&amp;rsquo;t want an AI agent accidentally deleting a production pool. Here are the layers of safety built in:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Guardrail&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent&lt;/strong>&lt;/td>
&lt;td>System prompt instructs the LLM to confirm before destructive ops, check dependencies, verify HA status&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong>&lt;/td>
&lt;td>&lt;code>requireApproval&lt;/code> on 10 destructive tools — execution pauses until human approves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Telegram&lt;/strong>&lt;/td>
&lt;td>HITL approval rendered as inline Approve/Reject buttons&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Wrapper&lt;/strong>&lt;/td>
&lt;td>&lt;code>READ_ONLY=true&lt;/code> mode blocks all write operations at the API level&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Wrapper&lt;/strong>&lt;/td>
&lt;td>&lt;code>ALLOWED_PARTITIONS&lt;/code> restricts which F5 partitions can be accessed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Network&lt;/strong>&lt;/td>
&lt;td>NetworkPolicy limits which pods can reach the wrapper and where the wrapper can connect&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="what-it-looks-like-in-practice">What It Looks Like in Practice&lt;/h2>
&lt;p>Here&amp;rsquo;s a typical conversation flow:&lt;/p>
&lt;p>&lt;strong>You&lt;/strong> (in Telegram): &amp;ldquo;Show me all pools on the F5&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Lists pools in a clean table with member counts, monitors, and load balancing methods.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Disable member 10.0.1.50:80 in pool k8s-argocd&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;I&amp;rsquo;ll disable member 10.0.1.50:80 in pool k8s-argocd. This will stop it from receiving new connections but allow existing connections to drain.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Telegram shows&lt;/strong>: &lt;code>[Approve] [Reject]&lt;/code> buttons&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: Tap &lt;code>Approve&lt;/code>&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;Done. Member 10.0.1.50:80 in pool k8s-argocd is now disabled (session: user-disabled, state: user-up).&amp;rdquo;&lt;/p>
&lt;h2 id="cicd">CI/CD&lt;/h2>
&lt;p>A GitHub Actions workflow automatically builds and pushes the &lt;code>f5-wrapper&lt;/code> Docker image to Docker Hub whenever files in &lt;code>apps/f5-wrapper/&lt;/code> change on &lt;code>main&lt;/code>. PRs build the image for validation but don&amp;rsquo;t push.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>This project brings together several patterns that I think are the future of infrastructure management:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP as the integration layer&lt;/strong> — instead of writing bespoke integrations, wrap existing APIs as MCP servers and let any AI agent discover and use them&lt;/li>
&lt;li>&lt;strong>kagent for agent lifecycle&lt;/strong> — define agents as Kubernetes CRDs, manage them with GitOps, get HITL approval for free&lt;/li>
&lt;li>&lt;strong>A2A for agent communication&lt;/strong> — the Telegram bot doesn&amp;rsquo;t know anything about F5; it just speaks A2A to kagent, which routes to the right agent&lt;/li>
&lt;li>&lt;strong>Defense in depth&lt;/strong> — read-only mode, partition allow-lists, HITL approval, network policies, and agent-level behavioral guidelines&lt;/li>
&lt;/ul>
&lt;p>The F5 wrapper has 28 tools covering pools, virtual servers, nodes, monitors, iRules, certificates, and system operations. You could extend it with more tools (data groups, policies, WAF rules) by adding more &lt;code>@mcp.tool()&lt;/code> functions — kagent discovers them automatically.&lt;/p>
&lt;p>The full source code is available in the &lt;a href="https://github.com/ProfessorSeb/k8s-iceman">k8s-iceman&lt;/a> repository:&lt;/p>
&lt;ul>
&lt;li>F5 wrapper: &lt;code>apps/f5-wrapper/&lt;/code>&lt;/li>
&lt;li>Agent manifests: &lt;code>manifests/kagent-examples/f5-agent/&lt;/code>&lt;/li>
&lt;li>Telegram bot manifests: &lt;code>manifests/kagent-examples/telegram-f5-bot/&lt;/code>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h1 id="managing-f5-big-ip-from-telegram-with-an-ai-agent-mcp-and-kagent">Managing F5 BIG-IP from Telegram with an AI Agent, MCP, and kagent&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>What if you could manage your F5 BIG-IP load balancer from Telegram? Not by SSH-ing into the box and running &lt;code>tmsh&lt;/code> — but by telling an AI agent in plain English to &amp;ldquo;show me all pools&amp;rdquo; or &amp;ldquo;disable node 10.0.1.25&amp;rdquo; and having it figure out the right iControl REST calls, execute them safely, and ask for your approval before anything destructive happens?&lt;/p>
&lt;p>That&amp;rsquo;s exactly what I built. In this article, I&amp;rsquo;ll walk through the full stack: a custom &lt;strong>MCP (Model Context Protocol) server&lt;/strong> that wraps the F5 iControl REST API, a &lt;strong>kagent AI agent&lt;/strong> on Kubernetes that uses those tools to manage the BIG-IP, and a &lt;strong>Telegram bot&lt;/strong> that lets you talk to the agent from your phone — complete with &lt;strong>Human-in-the-Loop (HITL) approval buttons&lt;/strong> for destructive operations.&lt;/p>
&lt;p>Everything runs on my home lab Kubernetes cluster (Talos Linux on Proxmox), deployed via GitOps with ArgoCD, with secrets managed by HashiCorp Vault.&lt;/p>
&lt;h2 id="why-not-just-use-the-f5-api-directly">Why Not Just Use the F5 API Directly?&lt;/h2>
&lt;p>The F5 iControl REST API is massive. It uses token-based auth with expiring sessions, returns deeply nested JSON, and has hundreds of endpoints. Talking to it directly from an AI agent would be fragile and wasteful.&lt;/p>
&lt;p>I needed a wrapper that:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Exposes only what the agent needs&lt;/strong> — a curated set of pool, virtual server, node, monitor, iRule, certificate, and system operations instead of the full iControl surface area&lt;/li>
&lt;li>&lt;strong>Handles auth in one place&lt;/strong> — token acquisition, automatic refresh before the 1200s expiry, and clean logout on shutdown&lt;/li>
&lt;li>&lt;strong>Speaks MCP natively&lt;/strong> — so kagent discovers all tools automatically via a single endpoint, no manual tool registration needed&lt;/li>
&lt;li>&lt;strong>Adds guardrails&lt;/strong> — read-only mode, partition allow-lists, HITL approval for destructive operations, and clear error responses the agent can reason about&lt;/li>
&lt;/ol>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s how it all fits together:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────┬───────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">polling&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌──────────────┐&lt;/span> &lt;span class="n">A2A&lt;/span> &lt;span class="n">protocol&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">──────────────────────▶│&lt;/span> &lt;span class="n">kagent&lt;/span> &lt;span class="n">Controller&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">K8s&lt;/span> &lt;span class="n">pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└──────────────┘&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">routes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">f5&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bigip&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">LLM&lt;/span> &lt;span class="n">decides&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">which&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="n">streamable&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">HTTP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">F5&lt;/span> &lt;span class="n">Wrapper&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">FastAPI&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">MCP&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└────────┬─────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">iControl&lt;/span> &lt;span class="n">REST&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">▼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">┌──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span> &lt;span class="n">F5&lt;/span> &lt;span class="n">BIG&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="ne">IP&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">└──────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Four components, each running as a separate pod in the &lt;code>kagent&lt;/code> namespace:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Telegram Bot&lt;/strong> — polls for messages, sends them to kagent via A2A, renders HITL approval buttons&lt;/li>
&lt;li>&lt;strong>kagent Controller&lt;/strong> — routes the request to the correct agent, manages conversation state&lt;/li>
&lt;li>&lt;strong>f5-bigip-agent&lt;/strong> — the AI agent (LLM) that decides which MCP tools to call based on the user&amp;rsquo;s natural language request&lt;/li>
&lt;li>&lt;strong>F5 Wrapper&lt;/strong> — FastAPI service that translates MCP tool calls into iControl REST API calls against the BIG-IP&lt;/li>
&lt;/ul>
&lt;h2 id="part-1-the-f5-mcp-wrapper">Part 1: The F5 MCP Wrapper&lt;/h2>
&lt;p>The wrapper is a Python FastAPI application that serves two purposes: a REST API for direct HTTP access and an MCP server for kagent tool discovery.&lt;/p>
&lt;h3 id="project-structure">Project Structure&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">f5&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">wrapper&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">├──&lt;/span> &lt;span class="n">Dockerfile&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">├──&lt;/span> &lt;span class="n">requirements&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">txt&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">├──&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">main&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># Starlette root — mounts REST + MCP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">mcp_server&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># 28 MCP tool definitions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">config&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># Pydantic Settings (env vars)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">auth&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># F5 token manager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">routers&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="c1"># REST API endpoints&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">pools&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">virtual_servers&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">nodes&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">monitors&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">irules&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">├──&lt;/span> &lt;span class="n">certificates&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">└──&lt;/span> &lt;span class="n">system&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└──&lt;/span> &lt;span class="n">utils&lt;/span>&lt;span class="o">/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└──&lt;/span> &lt;span class="n">f5_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">py&lt;/span> &lt;span class="c1"># Reusable HTTP client for iControl REST&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="authentication-token-lifecycle">Authentication: Token Lifecycle&lt;/h3>
&lt;p>The F5 iControl REST API uses token-based auth. Rather than handling this in every tool, the wrapper manages the entire token lifecycle transparently:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">F5TokenManager&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">def&lt;/span> &lt;span class="fm">__init__&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">host&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">password&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">verify_ssl&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">False&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">host&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">host&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_expiry&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">login&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/mgmt/shared/authn/login&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;username&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">username&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;password&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">password&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;loginProviderName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;tmos&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()[&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">][&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Tokens last 1200s; refresh at 80% (960s)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_expiry&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">960&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">get_token&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token_expiry&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">login&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">logout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Clean up token on shutdown&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/mgmt/shared/authz/tokens/&lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;X-F5-Auth-Token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="bp">self&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On startup, the app logs in and stores the token. Every request checks if the token is about to expire and refreshes automatically. On shutdown, the token is deleted.&lt;/p>
&lt;h3 id="mcp-tool-definitions-28-tools">MCP Tool Definitions (28 Tools)&lt;/h3>
&lt;p>The MCP server uses the &lt;code>FastMCP&lt;/code> library to expose tools that kagent discovers automatically. Here&amp;rsquo;s the full tool inventory:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Category&lt;/th>
&lt;th>Tools&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Pools&lt;/strong>&lt;/td>
&lt;td>8&lt;/td>
&lt;td>List, create, delete pools; add/remove/enable/disable members&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Virtual Servers&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>List, get details, create, delete virtual servers&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Nodes&lt;/strong>&lt;/td>
&lt;td>5&lt;/td>
&lt;td>List, get, create, delete nodes; enable/disable/force-offline&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Monitors&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>List all monitors, HTTP, HTTPS, and TCP monitors&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>iRules&lt;/strong>&lt;/td>
&lt;td>2&lt;/td>
&lt;td>List iRules, get full TCL definitions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Certificates&lt;/strong>&lt;/td>
&lt;td>2&lt;/td>
&lt;td>List SSL certs and expiration dates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>System&lt;/strong>&lt;/td>
&lt;td>3&lt;/td>
&lt;td>BIG-IP version/hostname, HA failover status, config sync status&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Each tool is a simple async function decorated with &lt;code>@mcp.tool()&lt;/code>. For example, here&amp;rsquo;s the pool listing tool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@mcp.tool&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">list_pools&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">partition&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Common&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;List all LTM pools in the specified partition.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">_client&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;/mgmt/tm/ltm/pool?$filter=partition eq &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">partition&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">items&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;items&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">item&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">items&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;partition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;partition&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Common&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;monitor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;monitor&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;none&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;lb_method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;loadBalancingMode&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;round-robin&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And a write operation with read-only guard:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@mcp.tool&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">create_pool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">partition&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Common&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">monitor&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/Common/http&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lb_method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;round-robin&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">members&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Create a new LTM pool with optional members.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">READ_ONLY&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;ERROR: Service is in read-only mode&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;partition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">partition&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;monitor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">monitor&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;loadBalancingMode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">lb_method&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">members&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;members&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">loads&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">members&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">members&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="n">members&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">_client&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/mgmt/tm/ltm/pool&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">payload&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">json&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">dumps&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">indent&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="mounting-mcp--rest-together">Mounting MCP + REST Together&lt;/h3>
&lt;p>The app uses Starlette as the root application to mount both the MCP server and the FastAPI REST API under different paths:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># MCP streamable-HTTP app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mcp_app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">mcp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">streamable_http_app&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Main Starlette app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Starlette&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lifespan&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">lifespan&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">routes&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Route&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/health&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">health&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/mcp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># MCP tools for kagent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Mount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">app&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">rest_app&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="c1"># REST API + Swagger docs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The lifespan handler initializes the F5 token manager on startup and triggers the MCP sub-app&amp;rsquo;s lifespan to set up its async task group:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@asynccontextmanager&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">lifespan&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">F5TokenManager&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">host&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">settings&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">F5_HOST&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">tm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">login&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">set_token_manager&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tm&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">mcp_app&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">router&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lifespan_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">mcp_app&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">yield&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">tm&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">logout&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="configuration">Configuration&lt;/h3>
&lt;p>All configuration is via environment variables, loaded by Pydantic Settings:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Variable&lt;/th>
&lt;th>Required&lt;/th>
&lt;th>Default&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>F5_HOST&lt;/code>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>—&lt;/td>
&lt;td>BIG-IP management URL&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_USERNAME&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>admin&lt;/code>&lt;/td>
&lt;td>iControl REST username&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_PASSWORD&lt;/code>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>—&lt;/td>
&lt;td>iControl REST password&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_VERIFY_SSL&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>Verify F5 SSL certificate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>F5_PARTITION&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>Common&lt;/code>&lt;/td>
&lt;td>Default BIG-IP partition&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>ALLOWED_PARTITIONS&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>Common&lt;/code>&lt;/td>
&lt;td>Partition allow-list&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>READ_ONLY&lt;/code>&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;td>Block all write operations&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="part-2-the-kagent-agent">Part 2: The kagent Agent&lt;/h2>
&lt;p>The AI agent is defined as a Kubernetes CRD — a declarative &lt;code>Agent&lt;/code> resource that tells kagent what the agent can do, which tools it has, and which operations need human approval.&lt;/p>
&lt;h3 id="agent-crd">Agent CRD&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;AI agent for managing F5 BIG-IP load balancer&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are f5-bigip-agent, an expert AI assistant for managing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> the F5 BIG-IP load balancer.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ## Behavioral Guidelines
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 1. Always confirm destructive operations before executing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 2. Check dependencies before deleting (e.g., is a VS using this pool?)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 3. Check HA failover status before write operations
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> 4. Use tables for listing multiple resources
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ...&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="hitl-approval">HITL Approval&lt;/h3>
&lt;p>The critical piece is &lt;code>requireApproval&lt;/code> — this tells kagent that 10 destructive operations must get human approval before execution:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_pool&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_pool&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">add_pool_member&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">remove_pool_member&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">set_pool_member_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_virtual_server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_virtual_server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">create_node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">delete_node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">set_node_state&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When the LLM decides to call one of these tools, kagent pauses execution and sends an approval request back through the A2A protocol. The Telegram bot renders this as Approve/Reject buttons.&lt;/p>
&lt;h3 id="a2a-skills">A2A Skills&lt;/h3>
&lt;p>The agent also advertises its capabilities via A2A skills, so other agents or clients can discover what it does:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">a2aConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">skills&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5 BIG-IP Operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Manage pools, virtual servers, nodes, monitors,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> and system status on F5 BIG-IP&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">examples&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Show me all pools on the F5&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Disable member 10.0.1.25:80 in pool prod-web&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;What is the failover status of the BIG-IP?&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-3-the-telegram-bot">Part 3: The Telegram Bot&lt;/h2>
&lt;p>The Telegram bot is the same Python bot I built for &lt;a href="https://maniak.io/articles/2026-03-13-telegram-bot-kagent-a2a-kubernetes/">general kagent operations&lt;/a>, redeployed with a different environment variable pointing it at the F5 agent:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-f5-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">KAGENT_A2A_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/f5-bigip-agent/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it — same bot image (&lt;code>sebbycorp/telegram-kagent-bot:latest&lt;/code>), different agent URL. The bot polls Telegram for messages, sends them to the F5 agent via A2A, and streams the response back to the chat. When a HITL approval is needed, it shows inline Approve/Reject buttons.&lt;/p>
&lt;p>The deployment uses &lt;code>strategy: Recreate&lt;/code> because Telegram only allows one poller per bot token.&lt;/p>
&lt;h2 id="part-4-kubernetes-deployment">Part 4: Kubernetes Deployment&lt;/h2>
&lt;p>The entire stack is deployed via GitOps. The manifests live in the &lt;code>k8s-iceman&lt;/code> repo under &lt;code>manifests/kagent-examples/&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">manifests/kagent-examples/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── f5-agent/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── bigip/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 01-external-secret.yaml # F5 creds from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 02-agent.yaml # kagent Agent CRD
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 03-deployment.yaml # Deployment + Service + MCP + NetworkPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── telegram-f5-bot/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── 01-external-secret.yaml # Telegram token from Vault
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── 02-deployment.yaml # Telegram bot Deployment
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ArgoCD watches this directory and auto-syncs any changes.&lt;/p>
&lt;h3 id="secrets-from-vault">Secrets from Vault&lt;/h3>
&lt;p>F5 credentials are stored in HashiCorp Vault and injected via External Secrets Operator:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-bigip-credentials&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5_HOST&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/f5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">host&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5_USERNAME&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/f5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">username&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">F5_PASSWORD&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret/f5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">password&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="network-policy">Network Policy&lt;/h3>
&lt;p>The wrapper is locked down with a NetworkPolicy that only allows ingress from the kagent namespace and egress to the F5 management IP + DNS:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NetworkPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-wrapper-bigip-policy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">podSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">f5-wrapper-bigip&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kubernetes.io/metadata.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">egress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">ipBlock&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cidr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.0.0.0&lt;/span>&lt;span class="l">/8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TCP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">to&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">namespaceSelector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">UDP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">53&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="safety-guardrails">Safety Guardrails&lt;/h2>
&lt;p>This is network infrastructure — you don&amp;rsquo;t want an AI agent accidentally deleting a production pool. Here are the layers of safety built in:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Layer&lt;/th>
&lt;th>Guardrail&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Agent&lt;/strong>&lt;/td>
&lt;td>System prompt instructs the LLM to confirm before destructive ops, check dependencies, verify HA status&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>kagent&lt;/strong>&lt;/td>
&lt;td>&lt;code>requireApproval&lt;/code> on 10 destructive tools — execution pauses until human approves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Telegram&lt;/strong>&lt;/td>
&lt;td>HITL approval rendered as inline Approve/Reject buttons&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Wrapper&lt;/strong>&lt;/td>
&lt;td>&lt;code>READ_ONLY=true&lt;/code> mode blocks all write operations at the API level&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Wrapper&lt;/strong>&lt;/td>
&lt;td>&lt;code>ALLOWED_PARTITIONS&lt;/code> restricts which F5 partitions can be accessed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Network&lt;/strong>&lt;/td>
&lt;td>NetworkPolicy limits which pods can reach the wrapper and where the wrapper can connect&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="what-it-looks-like-in-practice">What It Looks Like in Practice&lt;/h2>
&lt;p>Here&amp;rsquo;s a typical conversation flow:&lt;/p>
&lt;p>&lt;strong>You&lt;/strong> (in Telegram): &amp;ldquo;Show me all pools on the F5&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: Lists pools in a clean table with member counts, monitors, and load balancing methods.&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: &amp;ldquo;Disable member 10.0.1.50:80 in pool k8s-argocd&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;I&amp;rsquo;ll disable member 10.0.1.50:80 in pool k8s-argocd. This will stop it from receiving new connections but allow existing connections to drain.&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Telegram shows&lt;/strong>: &lt;code>[Approve] [Reject]&lt;/code> buttons&lt;/p>
&lt;p>&lt;strong>You&lt;/strong>: Tap &lt;code>Approve&lt;/code>&lt;/p>
&lt;p>&lt;strong>Agent&lt;/strong>: &amp;ldquo;Done. Member 10.0.1.50:80 in pool k8s-argocd is now disabled (session: user-disabled, state: user-up).&amp;rdquo;&lt;/p>
&lt;h2 id="cicd">CI/CD&lt;/h2>
&lt;p>A GitHub Actions workflow automatically builds and pushes the &lt;code>f5-wrapper&lt;/code> Docker image to Docker Hub whenever files in &lt;code>apps/f5-wrapper/&lt;/code> change on &lt;code>main&lt;/code>. PRs build the image for validation but don&amp;rsquo;t push.&lt;/p>
&lt;h2 id="wrapping-up">Wrapping Up&lt;/h2>
&lt;p>This project brings together several patterns that I think are the future of infrastructure management:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP as the integration layer&lt;/strong> — instead of writing bespoke integrations, wrap existing APIs as MCP servers and let any AI agent discover and use them&lt;/li>
&lt;li>&lt;strong>kagent for agent lifecycle&lt;/strong> — define agents as Kubernetes CRDs, manage them with GitOps, get HITL approval for free&lt;/li>
&lt;li>&lt;strong>A2A for agent communication&lt;/strong> — the Telegram bot doesn&amp;rsquo;t know anything about F5; it just speaks A2A to kagent, which routes to the right agent&lt;/li>
&lt;li>&lt;strong>Defense in depth&lt;/strong> — read-only mode, partition allow-lists, HITL approval, network policies, and agent-level behavioral guidelines&lt;/li>
&lt;/ul>
&lt;p>The F5 wrapper has 28 tools covering pools, virtual servers, nodes, monitors, iRules, certificates, and system operations. You could extend it with more tools (data groups, policies, WAF rules) by adding more &lt;code>@mcp.tool()&lt;/code> functions — kagent discovers them automatically.&lt;/p>
&lt;p>The full source code is available in the &lt;a href="https://github.com/ProfessorSeb/k8s-iceman">k8s-iceman&lt;/a> repository:&lt;/p>
&lt;ul>
&lt;li>F5 wrapper: &lt;code>apps/f5-wrapper/&lt;/code>&lt;/li>
&lt;li>Agent manifests: &lt;code>manifests/kagent-examples/f5-agent/&lt;/code>&lt;/li>
&lt;li>Telegram bot manifests: &lt;code>manifests/kagent-examples/telegram-f5-bot/&lt;/code>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Building a Telegram Bot for Your Kubernetes Cluster with kagent and A2A</title><link>https://maniak.io/articles/2026-03-13-telegram-bot-kagent-a2a-kubernetes/</link><pubDate>Fri, 13 Mar 2026 14:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-13-telegram-bot-kagent-a2a-kubernetes/</guid><description>&lt;h1 id="building-a-telegram-bot-for-your-kubernetes-cluster-with-kagent-and-a2a">Building a Telegram Bot for Your Kubernetes Cluster with kagent and A2A&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>What if you could manage your Kubernetes cluster from Telegram? Not through a half-baked webhook that runs &lt;code>kubectl&lt;/code> — but through a real AI agent that understands context, uses tools, responds intelligently, and &lt;strong>asks for your approval before doing anything destructive&lt;/strong>?&lt;/p>
&lt;p>In this article, I&amp;rsquo;ll walk you through how I built exactly that: a Telegram bot that connects to a &lt;a href="https://kagent.dev">kagent&lt;/a> AI agent running on my home lab Kubernetes cluster (Talos Linux on Proxmox), giving me full cluster operations from my phone. The entire thing is deployed via GitOps with ArgoCD, secrets come from HashiCorp Vault, and the bot uses the &lt;strong>A2A (Agent-to-Agent) protocol&lt;/strong> to communicate with kagent.&lt;/p>
&lt;p>The bot maintains &lt;strong>conversation continuity&lt;/strong> across messages (so the agent remembers what you were talking about), and supports &lt;strong>Human-in-the-Loop (HITL) approval&lt;/strong> — when the agent wants to run a destructive operation like deleting a resource or applying a manifest, it shows you Approve/Reject buttons in Telegram before proceeding.&lt;/p>
&lt;p>No webhooks. No public endpoints. Just polling from inside the cluster.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-overview">Architecture Overview&lt;/h2>
&lt;p>Here&amp;rsquo;s what we&amp;rsquo;re building:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌─────────────────────────────────────────────────────────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Cloud&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="n">API&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Long&lt;/span> &lt;span class="n">Polling&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└─────────────────────────────┼───────────────────────────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌─────────────────────────────┼───────────────────────────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Kubernetes&lt;/span> &lt;span class="n">Cluster&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">maniak&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">iceman&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">▼&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">python&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">httpx&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Tracks&lt;/span> &lt;span class="n">contextId&lt;/span> &lt;span class="n">per&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">user&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">session&lt;/span> &lt;span class="n">continuity&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────────┬─────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">HTTP&lt;/span> &lt;span class="n">POST&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">A2A&lt;/span> &lt;span class="n">JSON&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">RPC&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">send&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">contextId&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">▼&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">kagent&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">controller&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="p">:&lt;/span>&lt;span class="mi">8083&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">api&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">a2a&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kagent&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">k8s&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────────┬─────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Routes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">Agent&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">▼&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────────────┐&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">k8s&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│─────▶│&lt;/span> &lt;span class="n">kagent&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="k">tool&lt;/span>&lt;span class="o">-&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">LLM&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">gpt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">5.4&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">server&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Agent&lt;/span> &lt;span class="n">CRD&lt;/span> &lt;span class="err">│◀─────│&lt;/span> &lt;span class="p">:&lt;/span>&lt;span class="mi">8084&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mcp&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">long&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">term&lt;/span> &lt;span class="n">memory&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">└──────────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">context&lt;/span> &lt;span class="n">compaction&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└──────────────────────────┘&lt;/span> &lt;span class="err">┌────────┴──────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Tools&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Response&lt;/span> &lt;span class="n">states&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_get_resources&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">completed&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_create_resource&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">input&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">required&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_apply_manifest&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">HITL&lt;/span> &lt;span class="n">approval&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_delete_resource&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_get_pod_logs&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_scale&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌───────────────┐&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Vault&lt;/span> &lt;span class="err">│───▶│&lt;/span> &lt;span class="n">ExternalSecret&lt;/span> &lt;span class="err">│──▶&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">token&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">secret&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Operator&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">└──────────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└───────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└─────────────────────────────────────────────────────────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key insight: &lt;strong>the Telegram bot doesn&amp;rsquo;t talk to the LLM directly&lt;/strong>. It sends messages to the kagent controller&amp;rsquo;s A2A endpoint, which routes them to the correct agent. The agent handles LLM orchestration, tool invocation, and response generation. The bot is a transport layer that also handles &lt;strong>session tracking&lt;/strong> (via &lt;code>contextId&lt;/code>) and &lt;strong>HITL approval&lt;/strong> (via Telegram inline keyboards).&lt;/p>
&lt;hr>
&lt;h2 id="the-a2a-protocol">The A2A Protocol&lt;/h2>
&lt;p>&lt;a href="https://google.github.io/A2A/">A2A (Agent-to-Agent)&lt;/a> is a Google-backed open protocol for agent interoperability. kagent implements A2A on its controller, meaning any A2A-compatible client can talk to any kagent agent.&lt;/p>
&lt;p>The protocol uses JSON-RPC 2.0 over HTTP. Here&amp;rsquo;s what a message exchange looks like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────┐ ┌───────────────────┐ ┌─────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Telegram │ │ kagent-controller │ │ Agent Pod │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Bot Pod │ │ (A2A endpoint) │ │ + LLM + MCP │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────┬─────┘ └─────────┬──────────┘ └──────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ POST /api/a2a/kagent/ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ telegram-k8s-agent/ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────────────┐ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;method&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;message/send&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;params&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;message&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;kind&amp;#34;: &amp;#34;message&amp;#34;,│ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;contextId&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;abc-123...&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;parts&amp;#34;: [{ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;kind&amp;#34;: &amp;#34;text&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;text&amp;#34;: &amp;#34;list │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ my pods&amp;#34; │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }] │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────────────┘ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ──────────────────────────────────▶│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ Forward to agent │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │─────────────────────────────────▶│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ LLM + Tool calls │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ (k8s_get_resources) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │◀─────────────────────────────────│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ◀──────────────────────────────────│ Response with artifacts │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────────────┐ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;result&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;contextId&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;abc-123...&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;status&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;state&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;completed&amp;#34; │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;artifacts&amp;#34;: [{ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;parts&amp;#34;: [{ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;kind&amp;#34;: &amp;#34;text&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;text&amp;#34;: &amp;#34;Here │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ are your │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ pods: ...&amp;#34; │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }] │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }] │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────────────┘ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="key-things-about-kagents-a2a-implementation">Key things about kagent&amp;rsquo;s A2A implementation&lt;/h3>
&lt;ul>
&lt;li>The method is &lt;strong>&lt;code>message/send&lt;/code>&lt;/strong> (not &lt;code>tasks/send&lt;/code> as in the older A2A draft spec)&lt;/li>
&lt;li>Parts use &lt;code>&amp;quot;kind&amp;quot;: &amp;quot;text&amp;quot;&lt;/code> (not &lt;code>&amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;&lt;/code>)&lt;/li>
&lt;li>The URL pattern is &lt;code>/api/a2a/{namespace}/{agent-name}/&lt;/code> — &lt;strong>trailing slash required&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Session continuity uses &lt;code>contextId&lt;/code> inside the &lt;code>message&lt;/code> object&lt;/strong> — not &lt;code>sessionId&lt;/code> in &lt;code>params&lt;/code>&lt;/li>
&lt;li>Responses include a &lt;code>status.state&lt;/code> field: &lt;code>&amp;quot;completed&amp;quot;&lt;/code> for normal responses, &lt;code>&amp;quot;input-required&amp;quot;&lt;/code> for HITL approval&lt;/li>
&lt;li>Completed responses have text in &lt;code>result.artifacts[].parts[]&lt;/code>&lt;/li>
&lt;li>Input-required responses have data in &lt;code>result.status.message.parts[]&lt;/code> and text in &lt;code>result.history[]&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="session-continuity-with-contextid">Session Continuity with &lt;code>contextId&lt;/code>&lt;/h3>
&lt;p>This is the most important (and least documented) part of kagent&amp;rsquo;s A2A protocol. To maintain a conversation across multiple messages, you must include a &lt;code>contextId&lt;/code> in the &lt;strong>message object&lt;/strong> itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;unique-message-id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message/send&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;previously-returned-context-id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;now scale it to 3 replicas&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On the first message, omit &lt;code>contextId&lt;/code> — kagent will generate one and return it in &lt;code>result.contextId&lt;/code>. Store that value and send it back on every subsequent message to continue the conversation.&lt;/p>
&lt;p>If you don&amp;rsquo;t send &lt;code>contextId&lt;/code>, every message starts a fresh session and the agent won&amp;rsquo;t remember what you were talking about.&lt;/p>
&lt;h3 id="response-states">Response States&lt;/h3>
&lt;p>kagent A2A responses come in two states:&lt;/p>
&lt;p>&lt;strong>&lt;code>completed&lt;/code>&lt;/strong> — The agent finished processing. The response text is in &lt;code>artifacts&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;abc-123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;completed&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;artifacts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Here are your pods...&amp;#34;&lt;/span>&lt;span class="p">}]}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>&lt;code>input-required&lt;/code>&lt;/strong> — The agent needs human input before proceeding. This happens when a tool in the &lt;code>requireApproval&lt;/code> list is about to be called. The response contains an &lt;code>adk_request_confirmation&lt;/code> data part describing which tool and what arguments:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;abc-123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;input-required&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;adk_request_confirmation&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;originalFunctionCall&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;k8s_create_resource&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;staging&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Namespace&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To approve or reject, send a follow-up message with &lt;code>&amp;quot;approved&amp;quot;&lt;/code> or &lt;code>&amp;quot;rejected&amp;quot;&lt;/code> using the same &lt;code>contextId&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="the-components">The Components&lt;/h2>
&lt;h3 id="1-externalsecret--pulling-the-bot-token-from-vault">1. ExternalSecret — Pulling the Bot Token from Vault&lt;/h3>
&lt;p>The Telegram bot token lives in HashiCorp Vault at &lt;code>secret/telegram&lt;/code> with key &lt;code>api_key&lt;/code>. The External Secrets Operator syncs it into a Kubernetes secret:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refreshInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1h&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">creationPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Owner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This follows the same pattern as the existing &lt;code>kagent-openai&lt;/code> and &lt;code>kagent-anthropic&lt;/code> secrets in the cluster. No hardcoded tokens in Git.&lt;/p>
&lt;h3 id="2-the-agent-crd">2. The Agent CRD&lt;/h3>
&lt;p>The &lt;code>telegram-k8s-agent&lt;/code> is a kagent &lt;code>Declarative&lt;/code> agent focused on K8s resource management. It has &lt;strong>long-term memory&lt;/strong> (vector-backed via SQLite), &lt;strong>context compaction&lt;/strong> for long conversations, and a &lt;strong>prompt template&lt;/strong> with kagent&amp;rsquo;s built-in safety guardrails:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-k8s-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kubernetes resource management agent accessible via Telegram bot&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">a2aConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">skills&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8s-operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Kubernetes Operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Create, apply, inspect, and manage Kubernetes resources&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">examples&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Create a new staging namespace&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Apply a deployment for nginx&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;What pods are running in the default namespace?&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Show me the events in namespace kagent&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Scale the nginx deployment to 3 replicas&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">kubernetes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Long-term memory — remembers user preferences, namespaces, and&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># past operations across conversations (vector-backed via SQLite).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-embed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Context compaction — automatically summarizes older messages in long&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># conversations so the agent doesn&amp;#39;t lose track during extended sessions.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compaction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">120000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">eventRetentionSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">overlapSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dataSources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-builtin-prompts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">alias&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">builtin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are {{.AgentName}}, a Kubernetes resource management agent accessible via Telegram.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{include &amp;#34;builtin/safety-guardrails&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{include &amp;#34;builtin/tool-usage-best-practices&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{include &amp;#34;builtin/kubernetes-context&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Your primary role is helping users create, apply, inspect, and manage Kubernetes resources.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You can:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Create namespaces, deployments, services, configmaps, and other K8s resources
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Apply YAML manifests to the cluster
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Inspect running resources (pods, deployments, services, events, logs)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Scale deployments and manage rollouts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Describe resources and troubleshoot issues
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Guidelines:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Keep responses concise and well-formatted for Telegram chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Use short code blocks for YAML and logs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Summarize large outputs — don&amp;#39;t dump full resource lists
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - When creating resources, confirm the namespace and resource details before applying
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Mutating operations (create, apply, delete, scale) require user approval — explain what you plan to do first
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Available tools: {{.ToolNames}}&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- Read / Inspect ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_pod_logs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resource_yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_available_api_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- Create / Apply / Mutate ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_create_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_scale&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_rollout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_label_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_annotate_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_create_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_scale&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Notable features:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>memory&lt;/code> with &lt;code>openai-embed&lt;/code>&lt;/strong>: The agent remembers context from past conversations using vector search. If you told it &amp;ldquo;I always work in the staging namespace&amp;rdquo; last week, it&amp;rsquo;ll remember.&lt;/li>
&lt;li>&lt;strong>&lt;code>context.compaction&lt;/code>&lt;/strong>: Long Telegram conversations won&amp;rsquo;t blow out the LLM context window. kagent automatically summarizes older messages when the token count exceeds 120k.&lt;/li>
&lt;li>&lt;strong>&lt;code>promptTemplate&lt;/code> with &lt;code>dataSources&lt;/code>&lt;/strong>: Pulls in kagent&amp;rsquo;s built-in safety guardrails and Kubernetes-aware prompts from a ConfigMap, so the system message stays DRY.&lt;/li>
&lt;li>&lt;strong>&lt;code>requireApproval&lt;/code>&lt;/strong>: The four mutating tools (&lt;code>create&lt;/code>, &lt;code>apply&lt;/code>, &lt;code>delete&lt;/code>, &lt;code>scale&lt;/code>) trigger HITL — the bot shows Approve/Reject buttons in Telegram before they execute.&lt;/li>
&lt;/ul>
&lt;h3 id="3-the-bot-code">3. The Bot Code&lt;/h3>
&lt;p>The bot is ~460 lines of Python using &lt;code>python-telegram-bot&lt;/code> (polling mode) and &lt;code>httpx&lt;/code> for A2A calls. It handles three main concerns: &lt;strong>A2A communication with session continuity&lt;/strong>, &lt;strong>HITL approval flow via Telegram inline keyboards&lt;/strong>, and &lt;strong>robust response parsing&lt;/strong>.&lt;/p>
&lt;p>Let&amp;rsquo;s walk through it section by section.&lt;/p>
&lt;h4 id="a2a-communication">A2A Communication&lt;/h4>
&lt;p>The core of the bot — sending messages to kagent and tracking &lt;code>contextId&lt;/code> for session continuity:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Telegram bot that forwards messages to a kagent A2A agent.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">asyncio&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">os&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">uuid&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">httpx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">telegram&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">InlineKeyboardButton&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">InlineKeyboardMarkup&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Update&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">telegram.ext&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Application&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CallbackQueryHandler&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CommandHandler&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MessageHandler&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">filters&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">TELEGRAM_BOT_TOKEN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;TELEGRAM_BOT_TOKEN&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">KAGENT_A2A_URL&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;KAGENT_A2A_URL&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Per-user contextId for conversation continuity (maps Telegram user_id -&amp;gt; kagent contextId)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">user_contexts&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Pending approval tasks: callback_id -&amp;gt; {context_id, user_id}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">pending_approvals&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">_send_a2a_request&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">message_text&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Send a message to the kagent A2A endpoint and return the raw result.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">message&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;messageId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">uuid&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">uuid4&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">message_text&lt;/span>&lt;span class="p">}],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">context_id&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">message&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;messageId&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message/send&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AsyncClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">120.0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">KAGENT_A2A_URL&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">raise_for_status&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">result&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The critical detail: &lt;code>contextId&lt;/code> goes &lt;strong>inside the &lt;code>message&lt;/code> object&lt;/strong>, not in &lt;code>params&lt;/code>. On the first message, we omit it — kagent returns a &lt;code>contextId&lt;/code> in the result. We store that per Telegram user and send it on every subsequent message. This is what makes &amp;ldquo;tell me about nginx&amp;rdquo; followed by &amp;ldquo;now scale it to 3&amp;rdquo; work as a coherent conversation.&lt;/p>
&lt;h4 id="response-parsing">Response Parsing&lt;/h4>
&lt;p>kagent returns text in different locations depending on the response state. The bot tries three sources in order:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_extract_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Extract text from an A2A task result (artifacts, history, or status message).&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check artifacts first (completed responses)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">artifacts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;artifacts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">artifacts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">artifacts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">texts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">parts&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">texts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">texts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check history — last agent message with text parts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">msg&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">reversed&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;history&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;agent&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">texts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">texts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">texts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check status message&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[]):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why three sources? &lt;code>completed&lt;/code> responses put text in &lt;code>artifacts&lt;/code>. But &lt;code>input-required&lt;/code> responses (HITL) don&amp;rsquo;t have artifacts — the agent&amp;rsquo;s explanatory text is in the &lt;code>history&lt;/code> array. And some edge cases put short status messages in &lt;code>status.message.parts&lt;/code>. Without this fallback chain, you&amp;rsquo;d get &amp;ldquo;Agent returned no text response&amp;rdquo; for perfectly valid HITL interactions.&lt;/p>
&lt;h4 id="hitl-approval-flow">HITL Approval Flow&lt;/h4>
&lt;p>When kagent returns &lt;code>input-required&lt;/code>, the bot parses the &lt;code>adk_request_confirmation&lt;/code> data to figure out what tool the agent wants to run, then shows Telegram inline keyboard buttons:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_parse_adk_confirmation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Parse an adk_request_confirmation DataPart into a structured dict.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;adk_request_confirmation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">func_call&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;originalFunctionCall&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tool_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">func_call&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tool_args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">func_call&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;toolConfirmation&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">tool_name&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;ask_user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">questions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tool_args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;questions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">questions&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">questions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;question&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">questions&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ask_user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tool_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tool_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;questions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">questions&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hint&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;approval&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tool_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tool_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tool_args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tool_args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hint&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The bot classifies &lt;code>input-required&lt;/code> responses into three categories:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Approval&lt;/strong> — A tool from the &lt;code>requireApproval&lt;/code> list (e.g., &lt;code>k8s_create_resource&lt;/code>). Shows Approve/Reject buttons.&lt;/li>
&lt;li>&lt;strong>Ask user&lt;/strong> — The agent is using &lt;code>ask_user&lt;/code> to ask a clarifying question, sometimes with predefined choices.&lt;/li>
&lt;li>&lt;strong>Question&lt;/strong> — Generic fallback, prompts the user for free-text input.&lt;/li>
&lt;/ol>
&lt;p>When the user presses Approve, the bot sends &lt;code>&amp;quot;approved&amp;quot;&lt;/code> back to kagent with the same &lt;code>contextId&lt;/code>, and the agent proceeds to execute the tool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">handle_callback&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Handle inline keyboard button presses (approval and choice selection).&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">query&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">callback_query&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">answer&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">action&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">callback_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">approval&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pending_approvals&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pop&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">callback_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">approval&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;This action has expired or was already handled.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">approval&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;context_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">action&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;approve&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Approved. Processing...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">reply_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;approved&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">action&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;reject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Rejected.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">reply_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;rejected&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">action&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;choice&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">choice_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parts&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Selected: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">choice_value&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">reply_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">choice_value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Send the response back to kagent with the same contextId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">send_a2a_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">reply_text&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ... handle the follow-up result&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="message-handler">Message Handler&lt;/h4>
&lt;p>The main handler ties it all together — sends the user&amp;rsquo;s text to kagent, stores the returned &lt;code>contextId&lt;/code>, and dispatches to the right renderer:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">handle_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Forward user message to kagent A2A and reply with the response.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">effective_user&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">user_contexts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">thinking_msg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">reply_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Thinking...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">send_a2a_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_text&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_contexts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ctx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">_handle_a2a_result&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">user_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">thinking_msg&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">thinking_msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Error contacting agent: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>_handle_a2a_result&lt;/code> function checks &lt;code>status.state&lt;/code> — if it&amp;rsquo;s &lt;code>input-required&lt;/code>, it renders the approval UI; otherwise, it sends the text response.&lt;/p>
&lt;p>Key design decisions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Polling, not webhooks&lt;/strong>: No ingress, no public endpoint, no TLS termination. The bot makes outbound connections to Telegram&amp;rsquo;s API from inside the cluster.&lt;/li>
&lt;li>&lt;strong>&lt;code>contextId&lt;/code> per user&lt;/strong>: Each Telegram user&amp;rsquo;s conversation maps to a kagent &lt;code>contextId&lt;/code>. The &lt;code>/new&lt;/code> command clears it, starting a fresh session.&lt;/li>
&lt;li>&lt;strong>Inline keyboards for approvals&lt;/strong>: Instead of making the user type &amp;ldquo;yes&amp;rdquo; or &amp;ldquo;no&amp;rdquo;, the bot shows tappable buttons for HITL approval. Each button carries a callback ID that maps to the pending approval&amp;rsquo;s &lt;code>context_id&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Chunked responses&lt;/strong>: Telegram has a 4096-character message limit. Long agent responses are split into 4000-char chunks automatically.&lt;/li>
&lt;li>&lt;strong>Health file&lt;/strong>: A simple &lt;code>/tmp/bot-healthy&lt;/code> file is created on startup for Kubernetes liveness/readiness probes.&lt;/li>
&lt;/ul>
&lt;h3 id="4-the-deployment">4. The Deployment&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Recreate &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Only one poller at a time&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/sebbycorp/telegram-kagent-bot:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">KAGENT_A2A_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/telegram-k8s-agent/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LOG_LEVEL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;INFO&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">64Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">livenessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;import os; assert os.path.exists(&amp;#39;/tmp/bot-healthy&amp;#39;)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;import os; assert os.path.exists(&amp;#39;/tmp/bot-healthy&amp;#39;)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>Recreate&lt;/code> strategy is important — Telegram&amp;rsquo;s polling API doesn&amp;rsquo;t support multiple consumers. If you use &lt;code>RollingUpdate&lt;/code>, you&amp;rsquo;d briefly have two pods pulling the same messages.&lt;/p>
&lt;hr>
&lt;h2 id="the-hitl-approval-flow">The HITL Approval Flow&lt;/h2>
&lt;p>This is the most interesting part of the bot. Here&amp;rsquo;s what happens when you ask the agent to do something destructive:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐ ┌──────────────┐ ┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Telegram │ │ Bot (Pod) │ │ kagent A2A │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ User │ │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ &amp;#34;Create a staging namespace&amp;#34; │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │──────────────────────────────▶│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ message/send │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ (no contextId — first msg) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │─────────────────────────────▶│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ status: input-required │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ contextId: abc-123 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ data: adk_request_ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ confirmation { │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ k8s_create_resource │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ namespace: staging │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │◀─────────────────────────────│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ &amp;#34;The agent wants to run: │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ k8s_create_resource │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ namespace: staging&amp;#34; │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ [ Approve ] [ Reject ] │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │◀──────────────────────────────│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ *taps Approve* │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │──────────────────────────────▶│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ message/send │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ contextId: abc-123 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ text: &amp;#34;approved&amp;#34; │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │─────────────────────────────▶│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ status: completed │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;Namespace staging created&amp;#34; │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │◀─────────────────────────────│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ &amp;#34;Namespace staging created │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ successfully.&amp;#34; │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │◀──────────────────────────────│ │
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The same &lt;code>contextId&lt;/code> is used throughout the entire flow — from the initial request, through the approval, to the final result. This means the agent maintains context even across HITL interactions. You can ask &amp;ldquo;now deploy nginx to that namespace&amp;rdquo; and it&amp;rsquo;ll know you mean &lt;code>staging&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="gitops-flow">GitOps Flow&lt;/h2>
&lt;p>The entire deployment is managed through ArgoCD. Here&amp;rsquo;s how changes flow:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────┐ git push ┌──────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Developer │ ─────────────────▶│ GitHub │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (You) │ │ ProfessorSeb/ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────────┘ │ k8s-iceman │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┬──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ArgoCD polls │ (3 min)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ArgoCD │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ kagent-examples │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Application │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┬──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kubectl apply│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Kubernetes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├─ ExternalSecret │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├─ Agent CRD │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─ Deployment │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The repo structure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">k8s-iceman/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── apps/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── kagent-examples.yaml # ArgoCD Application (recurse: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── telegram-bot-src/ # Bot source code
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── Dockerfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── main.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── requirements.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── manifests/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── kagent-examples/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── telegram-bot/ # Picked up by ArgoCD automatically
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 01-external-secret.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 02-agent.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 03-deployment.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── helm-values/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── kagent/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── values.yaml # Model config (gpt-5.4)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Because the &lt;code>kagent-examples&lt;/code> ArgoCD Application has &lt;code>directory.recurse: true&lt;/code>, any new directory under &lt;code>manifests/kagent-examples/&lt;/code> is automatically synced. No new ArgoCD Application needed.&lt;/p>
&lt;hr>
&lt;h2 id="gotchas-and-lessons-learned">Gotchas and Lessons Learned&lt;/h2>
&lt;h3 id="1-contextid-not-sessionid">1. &lt;code>contextId&lt;/code>, not &lt;code>sessionId&lt;/code>&lt;/h3>
&lt;p>This was the biggest surprise. kagent&amp;rsquo;s A2A protocol uses &lt;code>contextId&lt;/code> for session continuity, and it must go &lt;strong>inside the &lt;code>message&lt;/code> object&lt;/strong> — not in &lt;code>params&lt;/code>, not as a top-level field. The A2A spec and other implementations sometimes mention &lt;code>sessionId&lt;/code> — kagent ignores it completely. I spent hours debugging why every message started a fresh conversation before diving into the &lt;a href="https://github.com/kagent-dev/kagent">kagent source code&lt;/a> and finding that &lt;code>contextId&lt;/code> maps directly to kagent&amp;rsquo;s internal session system.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># WRONG — kagent ignores sessionId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sessionId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># This does nothing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># RIGHT — contextId goes inside the message object&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># This maintains the session&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-input-required-responses-have-no-artifacts">2. &lt;code>input-required&lt;/code> responses have no artifacts&lt;/h3>
&lt;p>When the agent needs approval (HITL), the response has &lt;code>status.state: &amp;quot;input-required&amp;quot;&lt;/code> but &lt;strong>no &lt;code>artifacts&lt;/code> array&lt;/strong>. If you only parse &lt;code>artifacts&lt;/code>, you&amp;rsquo;ll get &amp;ldquo;Agent returned no text response&amp;rdquo; for every approval request. The agent&amp;rsquo;s explanation text is in the &lt;code>history&lt;/code> array (last agent message), and the approval data is in &lt;code>status.message.parts&lt;/code> as a &lt;code>data&lt;/code> kind part.&lt;/p>
&lt;h3 id="3-the-trailing-slash-matters">3. The trailing slash matters&lt;/h3>
&lt;p>The kagent A2A endpoint at &lt;code>/api/a2a/kagent/telegram-k8s-agent&lt;/code> returns a &lt;strong>307 redirect&lt;/strong> to &lt;code>/api/a2a/kagent/telegram-k8s-agent/&lt;/code>. The &lt;code>httpx&lt;/code> library (and most HTTP clients) won&amp;rsquo;t follow redirects on POST requests by default — for good security reasons. Always include the trailing slash.&lt;/p>
&lt;h3 id="4-kagent-uses-messagesend-not-taskssend">4. kagent uses &lt;code>message/send&lt;/code>, not &lt;code>tasks/send&lt;/code>&lt;/h3>
&lt;p>If you&amp;rsquo;re reading the A2A spec or looking at other A2A implementations, be aware that kagent uses &lt;code>message/send&lt;/code> as the method name. The older &lt;code>tasks/send&lt;/code> method returns a &lt;code>Method not found&lt;/code> error.&lt;/p>
&lt;h3 id="5-parts-use-kind-not-type">5. Parts use &lt;code>kind&lt;/code>, not &lt;code>type&lt;/code>&lt;/h3>
&lt;p>The A2A spec uses &lt;code>&amp;quot;kind&amp;quot;: &amp;quot;text&amp;quot;&lt;/code> for message parts. Some implementations and docs use &lt;code>&amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;&lt;/code>. kagent expects &lt;code>kind&lt;/code>.&lt;/p>
&lt;h3 id="6-telegram-message-limits">6. Telegram message limits&lt;/h3>
&lt;p>Telegram enforces a 4096-character limit per message. If your agent returns a large response (like a full pod listing or verbose logs), you need to chunk it. The bot handles this automatically by splitting at 4000-character boundaries.&lt;/p>
&lt;h3 id="7-recreate-strategy-not-rollingupdate">7. &lt;code>Recreate&lt;/code> strategy, not &lt;code>RollingUpdate&lt;/code>&lt;/h3>
&lt;p>Telegram&amp;rsquo;s long-polling API delivers each update to exactly one consumer. With two pods running during a rolling update, you&amp;rsquo;d get duplicate responses or missed messages. &lt;code>Recreate&lt;/code> ensures a clean handoff.&lt;/p>
&lt;hr>
&lt;h2 id="testing-it">Testing It&lt;/h2>
&lt;p>Once deployed, open Telegram and find your bot (the one you created with @BotFather):&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>/start&lt;/code>&lt;/strong> — Shows available commands&lt;/li>
&lt;li>&lt;strong>&lt;code>/status&lt;/code>&lt;/strong> — Checks connectivity to the kagent controller&lt;/li>
&lt;li>&lt;strong>&lt;code>/new&lt;/code>&lt;/strong> — Resets your conversation session (clears &lt;code>contextId&lt;/code>)&lt;/li>
&lt;li>&lt;strong>Send any message&lt;/strong> — It goes to the agent and comes back with a real answer&lt;/li>
&lt;/ol>
&lt;p>Example interactions:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> What pods are running in kagent namespace?&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> Here are the running pods in the kagent namespace:&lt;/p>
&lt;ul>
&lt;li>kagent-controller-57864fdf69-xfgmr (1/1 Running)&lt;/li>
&lt;li>telegram-bot-5469669bf-v8h7p (1/1 Running)&lt;/li>
&lt;li>telegram-k8s-agent-5f696bf4b9-pl7cl (1/1 Running)&lt;/li>
&lt;li>k8s-agent-6dccd8ddd8-gxt6x (1/1 Running)&lt;/li>
&lt;li>&amp;hellip; (28 more pods)&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> Create a staging namespace&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> The agent wants to run: k8s_create_resource
namespace: staging
kind: Namespace&lt;/p>
&lt;p>&lt;strong>[ Approve ]&lt;/strong> &lt;strong>[ Reject ]&lt;/strong>&lt;/p>
&lt;p>&lt;em>You tap Approve&lt;/em>&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> Namespace &lt;code>staging&lt;/code> created successfully.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> Now deploy nginx to that namespace&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> The agent wants to run: k8s_create_resource
namespace: staging
kind: Deployment
name: nginx
&amp;hellip;&lt;/p>
&lt;p>&lt;strong>[ Approve ]&lt;/strong> &lt;strong>[ Reject ]&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>Notice how the agent knows &amp;ldquo;that namespace&amp;rdquo; means &lt;code>staging&lt;/code> — because the &lt;code>contextId&lt;/code> maintained the conversation context across the approval interaction.&lt;/p>
&lt;hr>
&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;p>This is a foundation. From here you could:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Add more agents&lt;/strong>: Create a &lt;code>telegram-security-agent&lt;/code> with Kubescape tools, or a &lt;code>telegram-istio-agent&lt;/code> with mesh-specific tools&lt;/li>
&lt;li>&lt;strong>Route by command&lt;/strong>: Use different Telegram commands (&lt;code>/k8s&lt;/code>, &lt;code>/istio&lt;/code>, &lt;code>/security&lt;/code>) to route to different kagent agents&lt;/li>
&lt;li>&lt;strong>Add image support&lt;/strong>: kagent supports multi-modal parts — you could send screenshots of dashboards and ask &amp;ldquo;what&amp;rsquo;s wrong here?&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>Connect via agentgateway&lt;/strong>: Route the A2A traffic through &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> for rate limiting, authentication, and observability&lt;/li>
&lt;li>&lt;strong>Build other chat integrations&lt;/strong>: The A2A protocol is platform-agnostic. Swap &lt;code>python-telegram-bot&lt;/code> for &lt;code>discord.py&lt;/code>, &lt;code>slack-bolt&lt;/code>, or even a WhatsApp integration and the A2A layer stays exactly the same&lt;/li>
&lt;/ul>
&lt;p>The pattern works for any chat platform. The A2A protocol is the universal adapter.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The full source code and Kubernetes manifests are in the &lt;a href="https://github.com/ProfessorSeb/k8s-iceman">k8s-iceman&lt;/a> repo under &lt;code>apps/telegram-bot-src/&lt;/code> and &lt;code>manifests/kagent-examples/telegram-bot/&lt;/code>.&lt;/em>&lt;/p></description><content:encoded>&lt;h1 id="building-a-telegram-bot-for-your-kubernetes-cluster-with-kagent-and-a2a">Building a Telegram Bot for Your Kubernetes Cluster with kagent and A2A&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>What if you could manage your Kubernetes cluster from Telegram? Not through a half-baked webhook that runs &lt;code>kubectl&lt;/code> — but through a real AI agent that understands context, uses tools, responds intelligently, and &lt;strong>asks for your approval before doing anything destructive&lt;/strong>?&lt;/p>
&lt;p>In this article, I&amp;rsquo;ll walk you through how I built exactly that: a Telegram bot that connects to a &lt;a href="https://kagent.dev">kagent&lt;/a> AI agent running on my home lab Kubernetes cluster (Talos Linux on Proxmox), giving me full cluster operations from my phone. The entire thing is deployed via GitOps with ArgoCD, secrets come from HashiCorp Vault, and the bot uses the &lt;strong>A2A (Agent-to-Agent) protocol&lt;/strong> to communicate with kagent.&lt;/p>
&lt;p>The bot maintains &lt;strong>conversation continuity&lt;/strong> across messages (so the agent remembers what you were talking about), and supports &lt;strong>Human-in-the-Loop (HITL) approval&lt;/strong> — when the agent wants to run a destructive operation like deleting a resource or applying a manifest, it shows you Approve/Reject buttons in Telegram before proceeding.&lt;/p>
&lt;p>No webhooks. No public endpoints. Just polling from inside the cluster.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-overview">Architecture Overview&lt;/h2>
&lt;p>Here&amp;rsquo;s what we&amp;rsquo;re building:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌─────────────────────────────────────────────────────────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Cloud&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">Bot&lt;/span> &lt;span class="n">API&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────┬─────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Long&lt;/span> &lt;span class="n">Polling&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└─────────────────────────────┼───────────────────────────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌─────────────────────────────┼───────────────────────────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Kubernetes&lt;/span> &lt;span class="n">Cluster&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">maniak&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">iceman&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">▼&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">python&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">httpx&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Tracks&lt;/span> &lt;span class="n">contextId&lt;/span> &lt;span class="n">per&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Telegram&lt;/span> &lt;span class="n">user&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">session&lt;/span> &lt;span class="n">continuity&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────────┬─────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">HTTP&lt;/span> &lt;span class="n">POST&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">A2A&lt;/span> &lt;span class="n">JSON&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">RPC&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">send&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">contextId&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">▼&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">kagent&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">controller&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="p">:&lt;/span>&lt;span class="mi">8083&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">api&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">a2a&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">kagent&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">k8s&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────────┬─────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Routes&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">Agent&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">▼&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌──────────────────────────┐&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">k8s&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│─────▶│&lt;/span> &lt;span class="n">kagent&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="k">tool&lt;/span>&lt;span class="o">-&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">LLM&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">gpt&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">5.4&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">MCP&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">server&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">Pod&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Agent&lt;/span> &lt;span class="n">CRD&lt;/span> &lt;span class="err">│◀─────│&lt;/span> &lt;span class="p">:&lt;/span>&lt;span class="mi">8084&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">mcp&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">long&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">term&lt;/span> &lt;span class="n">memory&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">└──────────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">context&lt;/span> &lt;span class="n">compaction&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└──────────────────────────┘&lt;/span> &lt;span class="err">┌────────┴──────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Tools&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Response&lt;/span> &lt;span class="n">states&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_get_resources&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">completed&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_create_resource&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">input&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">required&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_apply_manifest&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">HITL&lt;/span> &lt;span class="n">approval&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_delete_resource&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_get_pod_logs&lt;/span>&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">•&lt;/span> &lt;span class="n">k8s_scale&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└────────────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">┌───────────────┐&lt;/span> &lt;span class="err">┌──────────────────┐&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Vault&lt;/span> &lt;span class="err">│───▶│&lt;/span> &lt;span class="n">ExternalSecret&lt;/span> &lt;span class="err">│──▶&lt;/span> &lt;span class="n">telegram&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">bot&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">token&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">secret&lt;/span>&lt;span class="o">/&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">Operator&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="n">telegram&lt;/span> &lt;span class="err">│&lt;/span> &lt;span class="err">└──────────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">└───────────────┘&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└─────────────────────────────────────────────────────────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key insight: &lt;strong>the Telegram bot doesn&amp;rsquo;t talk to the LLM directly&lt;/strong>. It sends messages to the kagent controller&amp;rsquo;s A2A endpoint, which routes them to the correct agent. The agent handles LLM orchestration, tool invocation, and response generation. The bot is a transport layer that also handles &lt;strong>session tracking&lt;/strong> (via &lt;code>contextId&lt;/code>) and &lt;strong>HITL approval&lt;/strong> (via Telegram inline keyboards).&lt;/p>
&lt;hr>
&lt;h2 id="the-a2a-protocol">The A2A Protocol&lt;/h2>
&lt;p>&lt;a href="https://google.github.io/A2A/">A2A (Agent-to-Agent)&lt;/a> is a Google-backed open protocol for agent interoperability. kagent implements A2A on its controller, meaning any A2A-compatible client can talk to any kagent agent.&lt;/p>
&lt;p>The protocol uses JSON-RPC 2.0 over HTTP. Here&amp;rsquo;s what a message exchange looks like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────┐ ┌───────────────────┐ ┌─────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Telegram │ │ kagent-controller │ │ Agent Pod │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Bot Pod │ │ (A2A endpoint) │ │ + LLM + MCP │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────┬─────┘ └─────────┬──────────┘ └──────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ POST /api/a2a/kagent/ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ telegram-k8s-agent/ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────────────┐ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;method&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;message/send&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;params&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;message&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;kind&amp;#34;: &amp;#34;message&amp;#34;,│ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;contextId&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;abc-123...&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;parts&amp;#34;: [{ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;kind&amp;#34;: &amp;#34;text&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;text&amp;#34;: &amp;#34;list │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ my pods&amp;#34; │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }] │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────────────┘ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ──────────────────────────────────▶│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ Forward to agent │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │─────────────────────────────────▶│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ LLM + Tool calls │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ (k8s_get_resources) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │◀─────────────────────────────────│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ◀──────────────────────────────────│ Response with artifacts │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌─────────────────────────┐ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;result&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;contextId&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;abc-123...&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;status&amp;#34;: { │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;state&amp;#34;: │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;completed&amp;#34; │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;artifacts&amp;#34;: [{ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;parts&amp;#34;: [{ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;kind&amp;#34;: &amp;#34;text&amp;#34;, │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;text&amp;#34;: &amp;#34;Here │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ are your │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ pods: ...&amp;#34; │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }] │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ }] │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─────────────────────────┘ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="key-things-about-kagents-a2a-implementation">Key things about kagent&amp;rsquo;s A2A implementation&lt;/h3>
&lt;ul>
&lt;li>The method is &lt;strong>&lt;code>message/send&lt;/code>&lt;/strong> (not &lt;code>tasks/send&lt;/code> as in the older A2A draft spec)&lt;/li>
&lt;li>Parts use &lt;code>&amp;quot;kind&amp;quot;: &amp;quot;text&amp;quot;&lt;/code> (not &lt;code>&amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;&lt;/code>)&lt;/li>
&lt;li>The URL pattern is &lt;code>/api/a2a/{namespace}/{agent-name}/&lt;/code> — &lt;strong>trailing slash required&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Session continuity uses &lt;code>contextId&lt;/code> inside the &lt;code>message&lt;/code> object&lt;/strong> — not &lt;code>sessionId&lt;/code> in &lt;code>params&lt;/code>&lt;/li>
&lt;li>Responses include a &lt;code>status.state&lt;/code> field: &lt;code>&amp;quot;completed&amp;quot;&lt;/code> for normal responses, &lt;code>&amp;quot;input-required&amp;quot;&lt;/code> for HITL approval&lt;/li>
&lt;li>Completed responses have text in &lt;code>result.artifacts[].parts[]&lt;/code>&lt;/li>
&lt;li>Input-required responses have data in &lt;code>result.status.message.parts[]&lt;/code> and text in &lt;code>result.history[]&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="session-continuity-with-contextid">Session Continuity with &lt;code>contextId&lt;/code>&lt;/h3>
&lt;p>This is the most important (and least documented) part of kagent&amp;rsquo;s A2A protocol. To maintain a conversation across multiple messages, you must include a &lt;code>contextId&lt;/code> in the &lt;strong>message object&lt;/strong> itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;unique-message-id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message/send&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;previously-returned-context-id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;now scale it to 3 replicas&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>On the first message, omit &lt;code>contextId&lt;/code> — kagent will generate one and return it in &lt;code>result.contextId&lt;/code>. Store that value and send it back on every subsequent message to continue the conversation.&lt;/p>
&lt;p>If you don&amp;rsquo;t send &lt;code>contextId&lt;/code>, every message starts a fresh session and the agent won&amp;rsquo;t remember what you were talking about.&lt;/p>
&lt;h3 id="response-states">Response States&lt;/h3>
&lt;p>kagent A2A responses come in two states:&lt;/p>
&lt;p>&lt;strong>&lt;code>completed&lt;/code>&lt;/strong> — The agent finished processing. The response text is in &lt;code>artifacts&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;abc-123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;completed&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;artifacts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Here are your pods...&amp;#34;&lt;/span>&lt;span class="p">}]}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>&lt;code>input-required&lt;/code>&lt;/strong> — The agent needs human input before proceeding. This happens when a tool in the &lt;code>requireApproval&lt;/code> list is about to be called. The response contains an &lt;code>adk_request_confirmation&lt;/code> data part describing which tool and what arguments:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;abc-123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;state&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;input-required&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;data&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;adk_request_confirmation&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;originalFunctionCall&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;k8s_create_resource&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;staging&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Namespace&amp;#34;&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To approve or reject, send a follow-up message with &lt;code>&amp;quot;approved&amp;quot;&lt;/code> or &lt;code>&amp;quot;rejected&amp;quot;&lt;/code> using the same &lt;code>contextId&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="the-components">The Components&lt;/h2>
&lt;h3 id="1-externalsecret--pulling-the-bot-token-from-vault">1. ExternalSecret — Pulling the Bot Token from Vault&lt;/h3>
&lt;p>The Telegram bot token lives in HashiCorp Vault at &lt;code>secret/telegram&lt;/code> with key &lt;code>api_key&lt;/code>. The External Secrets Operator syncs it into a Kubernetes secret:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">external-secrets.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ExternalSecret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refreshInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1h&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretStoreRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vault-backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterSecretStore&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">creationPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Owner&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">secretKey&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remoteRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">property&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api_key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This follows the same pattern as the existing &lt;code>kagent-openai&lt;/code> and &lt;code>kagent-anthropic&lt;/code> secrets in the cluster. No hardcoded tokens in Git.&lt;/p>
&lt;h3 id="2-the-agent-crd">2. The Agent CRD&lt;/h3>
&lt;p>The &lt;code>telegram-k8s-agent&lt;/code> is a kagent &lt;code>Declarative&lt;/code> agent focused on K8s resource management. It has &lt;strong>long-term memory&lt;/strong> (vector-backed via SQLite), &lt;strong>context compaction&lt;/strong> for long conversations, and a &lt;strong>prompt template&lt;/strong> with kagent&amp;rsquo;s built-in safety guardrails:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-k8s-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Kubernetes resource management agent accessible via Telegram bot&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">a2aConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">skills&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">k8s-operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Kubernetes Operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Create, apply, inspect, and manage Kubernetes resources&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">examples&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Create a new staging namespace&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Apply a deployment for nginx&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;What pods are running in the default namespace?&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Show me the events in namespace kagent&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;Scale the nginx deployment to 3 replicas&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">kubernetes&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">operations&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Long-term memory — remembers user preferences, namespaces, and&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># past operations across conversations (vector-backed via SQLite).&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-embed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Context compaction — automatically summarizes older messages in long&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># conversations so the agent doesn&amp;#39;t lose track during extended sessions.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">compaction&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">120000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">eventRetentionSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">overlapSize&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptTemplate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">dataSources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-builtin-prompts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">alias&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">builtin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are {{.AgentName}}, a Kubernetes resource management agent accessible via Telegram.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{include &amp;#34;builtin/safety-guardrails&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{include &amp;#34;builtin/tool-usage-best-practices&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> {{include &amp;#34;builtin/kubernetes-context&amp;#34;}}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Your primary role is helping users create, apply, inspect, and manage Kubernetes resources.
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You can:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Create namespaces, deployments, services, configmaps, and other K8s resources
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Apply YAML manifests to the cluster
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Inspect running resources (pods, deployments, services, events, logs)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Scale deployments and manage rollouts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Describe resources and troubleshoot issues
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Guidelines:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Keep responses concise and well-formatted for Telegram chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Use short code blocks for YAML and logs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Summarize large outputs — don&amp;#39;t dump full resource lists
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - When creating resources, confirm the namespace and resource details before applying
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> - Mutating operations (create, apply, delete, scale) require user approval — explain what you plan to do first
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Available tools: {{.ToolNames}}&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- Read / Inspect ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_pod_logs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resource_yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_available_api_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># --- Create / Apply / Mutate ---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_create_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_scale&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_rollout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_label_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_annotate_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_create_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_scale&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Notable features:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>memory&lt;/code> with &lt;code>openai-embed&lt;/code>&lt;/strong>: The agent remembers context from past conversations using vector search. If you told it &amp;ldquo;I always work in the staging namespace&amp;rdquo; last week, it&amp;rsquo;ll remember.&lt;/li>
&lt;li>&lt;strong>&lt;code>context.compaction&lt;/code>&lt;/strong>: Long Telegram conversations won&amp;rsquo;t blow out the LLM context window. kagent automatically summarizes older messages when the token count exceeds 120k.&lt;/li>
&lt;li>&lt;strong>&lt;code>promptTemplate&lt;/code> with &lt;code>dataSources&lt;/code>&lt;/strong>: Pulls in kagent&amp;rsquo;s built-in safety guardrails and Kubernetes-aware prompts from a ConfigMap, so the system message stays DRY.&lt;/li>
&lt;li>&lt;strong>&lt;code>requireApproval&lt;/code>&lt;/strong>: The four mutating tools (&lt;code>create&lt;/code>, &lt;code>apply&lt;/code>, &lt;code>delete&lt;/code>, &lt;code>scale&lt;/code>) trigger HITL — the bot shows Approve/Reject buttons in Telegram before they execute.&lt;/li>
&lt;/ul>
&lt;h3 id="3-the-bot-code">3. The Bot Code&lt;/h3>
&lt;p>The bot is ~460 lines of Python using &lt;code>python-telegram-bot&lt;/code> (polling mode) and &lt;code>httpx&lt;/code> for A2A calls. It handles three main concerns: &lt;strong>A2A communication with session continuity&lt;/strong>, &lt;strong>HITL approval flow via Telegram inline keyboards&lt;/strong>, and &lt;strong>robust response parsing&lt;/strong>.&lt;/p>
&lt;p>Let&amp;rsquo;s walk through it section by section.&lt;/p>
&lt;h4 id="a2a-communication">A2A Communication&lt;/h4>
&lt;p>The core of the bot — sending messages to kagent and tracking &lt;code>contextId&lt;/code> for session continuity:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Telegram bot that forwards messages to a kagent A2A agent.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">asyncio&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">logging&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">os&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">uuid&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">pathlib&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">httpx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">telegram&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">InlineKeyboardButton&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">InlineKeyboardMarkup&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Update&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">telegram.ext&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">Application&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CallbackQueryHandler&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CommandHandler&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">MessageHandler&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">filters&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">TELEGRAM_BOT_TOKEN&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;TELEGRAM_BOT_TOKEN&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">KAGENT_A2A_URL&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">environ&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;KAGENT_A2A_URL&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Per-user contextId for conversation continuity (maps Telegram user_id -&amp;gt; kagent contextId)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">user_contexts&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Pending approval tasks: callback_id -&amp;gt; {context_id, user_id}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">pending_approvals&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">_send_a2a_request&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">message_text&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Send a message to the kagent A2A endpoint and return the raw result.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">message&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;messageId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">uuid&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">uuid4&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">message_text&lt;/span>&lt;span class="p">}],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">context_id&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">message&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">context_id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;jsonrpc&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;messageId&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;message/send&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">message&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="n">httpx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">AsyncClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">timeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">120.0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">KAGENT_A2A_URL&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">json&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">payload&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">headers&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">raise_for_status&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">result&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The critical detail: &lt;code>contextId&lt;/code> goes &lt;strong>inside the &lt;code>message&lt;/code> object&lt;/strong>, not in &lt;code>params&lt;/code>. On the first message, we omit it — kagent returns a &lt;code>contextId&lt;/code> in the result. We store that per Telegram user and send it on every subsequent message. This is what makes &amp;ldquo;tell me about nginx&amp;rdquo; followed by &amp;ldquo;now scale it to 3&amp;rdquo; work as a coherent conversation.&lt;/p>
&lt;h4 id="response-parsing">Response Parsing&lt;/h4>
&lt;p>kagent returns text in different locations depending on the response state. The bot tries three sources in order:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_extract_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Extract text from an A2A task result (artifacts, history, or status message).&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check artifacts first (completed responses)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">artifacts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;artifacts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">artifacts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">artifacts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">texts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">parts&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">texts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">texts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check history — last agent message with text parts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">msg&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">reversed&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;history&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s2">&amp;#34;agent&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">texts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">)]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">texts&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">texts&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Check status message&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">p&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[]):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">p&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;text&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Why three sources? &lt;code>completed&lt;/code> responses put text in &lt;code>artifacts&lt;/code>. But &lt;code>input-required&lt;/code> responses (HITL) don&amp;rsquo;t have artifacts — the agent&amp;rsquo;s explanatory text is in the &lt;code>history&lt;/code> array. And some edge cases put short status messages in &lt;code>status.message.parts&lt;/code>. Without this fallback chain, you&amp;rsquo;d get &amp;ldquo;Agent returned no text response&amp;rdquo; for perfectly valid HITL interactions.&lt;/p>
&lt;h4 id="hitl-approval-flow">HITL Approval Flow&lt;/h4>
&lt;p>When kagent returns &lt;code>input-required&lt;/code>, the bot parses the &lt;code>adk_request_confirmation&lt;/code> data to figure out what tool the agent wants to run, then shows Telegram inline keyboard buttons:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_parse_adk_confirmation&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">data&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">dict&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">dict&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Parse an adk_request_confirmation DataPart into a structured dict.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;adk_request_confirmation&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">func_call&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;originalFunctionCall&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tool_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">func_call&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">tool_args&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">func_call&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">hint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;toolConfirmation&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{})&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">tool_name&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;ask_user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">questions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tool_args&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;questions&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">[])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">questions&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">questions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;question&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">questions&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ask_user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tool_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tool_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;questions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">questions&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hint&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;approval&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tool_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tool_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tool_args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">tool_args&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;hint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">hint&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">None&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The bot classifies &lt;code>input-required&lt;/code> responses into three categories:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Approval&lt;/strong> — A tool from the &lt;code>requireApproval&lt;/code> list (e.g., &lt;code>k8s_create_resource&lt;/code>). Shows Approve/Reject buttons.&lt;/li>
&lt;li>&lt;strong>Ask user&lt;/strong> — The agent is using &lt;code>ask_user&lt;/code> to ask a clarifying question, sometimes with predefined choices.&lt;/li>
&lt;li>&lt;strong>Question&lt;/strong> — Generic fallback, prompts the user for free-text input.&lt;/li>
&lt;/ol>
&lt;p>When the user presses Approve, the bot sends &lt;code>&amp;quot;approved&amp;quot;&lt;/code> back to kagent with the same &lt;code>contextId&lt;/code>, and the agent proceeds to execute the tool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">handle_callback&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Handle inline keyboard button presses (approval and choice selection).&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">query&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">callback_query&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">answer&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parts&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;:&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">action&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">callback_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">approval&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">pending_approvals&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">pop&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">callback_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">approval&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;This action has expired or was already handled.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">approval&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;context_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">action&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;approve&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Approved. Processing...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">reply_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;approved&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">action&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;reject&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Rejected.&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">reply_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;rejected&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="n">action&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;choice&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">choice_value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parts&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&lt;/span> &lt;span class="mi">2&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">query&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_message_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Selected: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">choice_value&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">reply_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">choice_value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># Send the response back to kagent with the same contextId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">send_a2a_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">reply_text&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># ... handle the follow-up result&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="message-handler">Message Handler&lt;/h4>
&lt;p>The main handler ties it all together — sends the user&amp;rsquo;s text to kagent, stores the returned &lt;code>contextId&lt;/code>, and dispatches to the right renderer:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">handle_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">update&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Update&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">_&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Forward user message to kagent A2A and reply with the response.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">effective_user&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">id&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_text&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">user_contexts&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">thinking_msg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">update&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">reply_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Thinking...&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">send_a2a_message&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">user_text&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_id&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">ctx&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_contexts&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ctx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">_handle_a2a_result&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">user_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">thinking_msg&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">thinking_msg&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">edit_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Error contacting agent: &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>_handle_a2a_result&lt;/code> function checks &lt;code>status.state&lt;/code> — if it&amp;rsquo;s &lt;code>input-required&lt;/code>, it renders the approval UI; otherwise, it sends the text response.&lt;/p>
&lt;p>Key design decisions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Polling, not webhooks&lt;/strong>: No ingress, no public endpoint, no TLS termination. The bot makes outbound connections to Telegram&amp;rsquo;s API from inside the cluster.&lt;/li>
&lt;li>&lt;strong>&lt;code>contextId&lt;/code> per user&lt;/strong>: Each Telegram user&amp;rsquo;s conversation maps to a kagent &lt;code>contextId&lt;/code>. The &lt;code>/new&lt;/code> command clears it, starting a fresh session.&lt;/li>
&lt;li>&lt;strong>Inline keyboards for approvals&lt;/strong>: Instead of making the user type &amp;ldquo;yes&amp;rdquo; or &amp;ldquo;no&amp;rdquo;, the bot shows tappable buttons for HITL approval. Each button carries a callback ID that maps to the pending approval&amp;rsquo;s &lt;code>context_id&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Chunked responses&lt;/strong>: Telegram has a 4096-character message limit. Long agent responses are split into 4000-char chunks automatically.&lt;/li>
&lt;li>&lt;strong>Health file&lt;/strong>: A simple &lt;code>/tmp/bot-healthy&lt;/code> file is created on startup for Kubernetes liveness/readiness probes.&lt;/li>
&lt;/ul>
&lt;h3 id="4-the-deployment">4. The Deployment&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Recreate &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Only one poller at a time&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bot&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/sebbycorp/telegram-kagent-bot:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">imagePullPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Always&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">valueFrom&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretKeyRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">telegram-bot-token&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TELEGRAM_BOT_TOKEN&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">KAGENT_A2A_URL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://kagent-controller.kagent.svc.cluster.local:8083/api/a2a/kagent/telegram-k8s-agent/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LOG_LEVEL&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;INFO&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">64Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">livenessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;import os; assert os.path.exists(&amp;#39;/tmp/bot-healthy&amp;#39;)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">python&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- -&lt;span class="l">c&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;import os; assert os.path.exists(&amp;#39;/tmp/bot-healthy&amp;#39;)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>Recreate&lt;/code> strategy is important — Telegram&amp;rsquo;s polling API doesn&amp;rsquo;t support multiple consumers. If you use &lt;code>RollingUpdate&lt;/code>, you&amp;rsquo;d briefly have two pods pulling the same messages.&lt;/p>
&lt;hr>
&lt;h2 id="the-hitl-approval-flow">The HITL Approval Flow&lt;/h2>
&lt;p>This is the most interesting part of the bot. Here&amp;rsquo;s what happens when you ask the agent to do something destructive:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐ ┌──────────────┐ ┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Telegram │ │ Bot (Pod) │ │ kagent A2A │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ User │ │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ &amp;#34;Create a staging namespace&amp;#34; │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │──────────────────────────────▶│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ message/send │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ (no contextId — first msg) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │─────────────────────────────▶│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ status: input-required │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ contextId: abc-123 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ data: adk_request_ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ confirmation { │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ k8s_create_resource │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ namespace: staging │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ } │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │◀─────────────────────────────│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ &amp;#34;The agent wants to run: │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ k8s_create_resource │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ namespace: staging&amp;#34; │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ [ Approve ] [ Reject ] │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │◀──────────────────────────────│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ *taps Approve* │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │──────────────────────────────▶│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ message/send │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ contextId: abc-123 │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ text: &amp;#34;approved&amp;#34; │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │─────────────────────────────▶│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ status: completed │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ &amp;#34;Namespace staging created&amp;#34; │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │◀─────────────────────────────│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ &amp;#34;Namespace staging created │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ successfully.&amp;#34; │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │◀──────────────────────────────│ │
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The same &lt;code>contextId&lt;/code> is used throughout the entire flow — from the initial request, through the approval, to the final result. This means the agent maintains context even across HITL interactions. You can ask &amp;ldquo;now deploy nginx to that namespace&amp;rdquo; and it&amp;rsquo;ll know you mean &lt;code>staging&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="gitops-flow">GitOps Flow&lt;/h2>
&lt;p>The entire deployment is managed through ArgoCD. Here&amp;rsquo;s how changes flow:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────┐ git push ┌──────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Developer │ ─────────────────▶│ GitHub │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (You) │ │ ProfessorSeb/ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─────────────┘ │ k8s-iceman │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┬──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ArgoCD polls │ (3 min)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ArgoCD │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ kagent-examples │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Application │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┬──────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> kubectl apply│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Kubernetes │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├─ ExternalSecret │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ├─ Agent CRD │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └─ Deployment │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The repo structure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">k8s-iceman/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── apps/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── kagent-examples.yaml # ArgoCD Application (recurse: true)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── telegram-bot-src/ # Bot source code
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── Dockerfile
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── main.py
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── requirements.txt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── manifests/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── kagent-examples/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── telegram-bot/ # Picked up by ArgoCD automatically
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 01-external-secret.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ├── 02-agent.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └── 03-deployment.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── helm-values/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── kagent/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── values.yaml # Model config (gpt-5.4)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Because the &lt;code>kagent-examples&lt;/code> ArgoCD Application has &lt;code>directory.recurse: true&lt;/code>, any new directory under &lt;code>manifests/kagent-examples/&lt;/code> is automatically synced. No new ArgoCD Application needed.&lt;/p>
&lt;hr>
&lt;h2 id="gotchas-and-lessons-learned">Gotchas and Lessons Learned&lt;/h2>
&lt;h3 id="1-contextid-not-sessionid">1. &lt;code>contextId&lt;/code>, not &lt;code>sessionId&lt;/code>&lt;/h3>
&lt;p>This was the biggest surprise. kagent&amp;rsquo;s A2A protocol uses &lt;code>contextId&lt;/code> for session continuity, and it must go &lt;strong>inside the &lt;code>message&lt;/code> object&lt;/strong> — not in &lt;code>params&lt;/code>, not as a top-level field. The A2A spec and other implementations sometimes mention &lt;code>sessionId&lt;/code> — kagent ignores it completely. I spent hours debugging why every message started a fresh conversation before diving into the &lt;a href="https://github.com/kagent-dev/kagent">kagent source code&lt;/a> and finding that &lt;code>contextId&lt;/code> maps directly to kagent&amp;rsquo;s internal session system.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># WRONG — kagent ignores sessionId&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;sessionId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># This does nothing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># RIGHT — contextId goes inside the message object&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">payload&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;params&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;contextId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># This maintains the session&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;parts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-input-required-responses-have-no-artifacts">2. &lt;code>input-required&lt;/code> responses have no artifacts&lt;/h3>
&lt;p>When the agent needs approval (HITL), the response has &lt;code>status.state: &amp;quot;input-required&amp;quot;&lt;/code> but &lt;strong>no &lt;code>artifacts&lt;/code> array&lt;/strong>. If you only parse &lt;code>artifacts&lt;/code>, you&amp;rsquo;ll get &amp;ldquo;Agent returned no text response&amp;rdquo; for every approval request. The agent&amp;rsquo;s explanation text is in the &lt;code>history&lt;/code> array (last agent message), and the approval data is in &lt;code>status.message.parts&lt;/code> as a &lt;code>data&lt;/code> kind part.&lt;/p>
&lt;h3 id="3-the-trailing-slash-matters">3. The trailing slash matters&lt;/h3>
&lt;p>The kagent A2A endpoint at &lt;code>/api/a2a/kagent/telegram-k8s-agent&lt;/code> returns a &lt;strong>307 redirect&lt;/strong> to &lt;code>/api/a2a/kagent/telegram-k8s-agent/&lt;/code>. The &lt;code>httpx&lt;/code> library (and most HTTP clients) won&amp;rsquo;t follow redirects on POST requests by default — for good security reasons. Always include the trailing slash.&lt;/p>
&lt;h3 id="4-kagent-uses-messagesend-not-taskssend">4. kagent uses &lt;code>message/send&lt;/code>, not &lt;code>tasks/send&lt;/code>&lt;/h3>
&lt;p>If you&amp;rsquo;re reading the A2A spec or looking at other A2A implementations, be aware that kagent uses &lt;code>message/send&lt;/code> as the method name. The older &lt;code>tasks/send&lt;/code> method returns a &lt;code>Method not found&lt;/code> error.&lt;/p>
&lt;h3 id="5-parts-use-kind-not-type">5. Parts use &lt;code>kind&lt;/code>, not &lt;code>type&lt;/code>&lt;/h3>
&lt;p>The A2A spec uses &lt;code>&amp;quot;kind&amp;quot;: &amp;quot;text&amp;quot;&lt;/code> for message parts. Some implementations and docs use &lt;code>&amp;quot;type&amp;quot;: &amp;quot;text&amp;quot;&lt;/code>. kagent expects &lt;code>kind&lt;/code>.&lt;/p>
&lt;h3 id="6-telegram-message-limits">6. Telegram message limits&lt;/h3>
&lt;p>Telegram enforces a 4096-character limit per message. If your agent returns a large response (like a full pod listing or verbose logs), you need to chunk it. The bot handles this automatically by splitting at 4000-character boundaries.&lt;/p>
&lt;h3 id="7-recreate-strategy-not-rollingupdate">7. &lt;code>Recreate&lt;/code> strategy, not &lt;code>RollingUpdate&lt;/code>&lt;/h3>
&lt;p>Telegram&amp;rsquo;s long-polling API delivers each update to exactly one consumer. With two pods running during a rolling update, you&amp;rsquo;d get duplicate responses or missed messages. &lt;code>Recreate&lt;/code> ensures a clean handoff.&lt;/p>
&lt;hr>
&lt;h2 id="testing-it">Testing It&lt;/h2>
&lt;p>Once deployed, open Telegram and find your bot (the one you created with @BotFather):&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>/start&lt;/code>&lt;/strong> — Shows available commands&lt;/li>
&lt;li>&lt;strong>&lt;code>/status&lt;/code>&lt;/strong> — Checks connectivity to the kagent controller&lt;/li>
&lt;li>&lt;strong>&lt;code>/new&lt;/code>&lt;/strong> — Resets your conversation session (clears &lt;code>contextId&lt;/code>)&lt;/li>
&lt;li>&lt;strong>Send any message&lt;/strong> — It goes to the agent and comes back with a real answer&lt;/li>
&lt;/ol>
&lt;p>Example interactions:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> What pods are running in kagent namespace?&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> Here are the running pods in the kagent namespace:&lt;/p>
&lt;ul>
&lt;li>kagent-controller-57864fdf69-xfgmr (1/1 Running)&lt;/li>
&lt;li>telegram-bot-5469669bf-v8h7p (1/1 Running)&lt;/li>
&lt;li>telegram-k8s-agent-5f696bf4b9-pl7cl (1/1 Running)&lt;/li>
&lt;li>k8s-agent-6dccd8ddd8-gxt6x (1/1 Running)&lt;/li>
&lt;li>&amp;hellip; (28 more pods)&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> Create a staging namespace&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> The agent wants to run: k8s_create_resource
namespace: staging
kind: Namespace&lt;/p>
&lt;p>&lt;strong>[ Approve ]&lt;/strong> &lt;strong>[ Reject ]&lt;/strong>&lt;/p>
&lt;p>&lt;em>You tap Approve&lt;/em>&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> Namespace &lt;code>staging&lt;/code> created successfully.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>You:&lt;/strong> Now deploy nginx to that namespace&lt;/p>
&lt;p>&lt;strong>Bot:&lt;/strong> The agent wants to run: k8s_create_resource
namespace: staging
kind: Deployment
name: nginx
&amp;hellip;&lt;/p>
&lt;p>&lt;strong>[ Approve ]&lt;/strong> &lt;strong>[ Reject ]&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>Notice how the agent knows &amp;ldquo;that namespace&amp;rdquo; means &lt;code>staging&lt;/code> — because the &lt;code>contextId&lt;/code> maintained the conversation context across the approval interaction.&lt;/p>
&lt;hr>
&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;p>This is a foundation. From here you could:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Add more agents&lt;/strong>: Create a &lt;code>telegram-security-agent&lt;/code> with Kubescape tools, or a &lt;code>telegram-istio-agent&lt;/code> with mesh-specific tools&lt;/li>
&lt;li>&lt;strong>Route by command&lt;/strong>: Use different Telegram commands (&lt;code>/k8s&lt;/code>, &lt;code>/istio&lt;/code>, &lt;code>/security&lt;/code>) to route to different kagent agents&lt;/li>
&lt;li>&lt;strong>Add image support&lt;/strong>: kagent supports multi-modal parts — you could send screenshots of dashboards and ask &amp;ldquo;what&amp;rsquo;s wrong here?&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>Connect via agentgateway&lt;/strong>: Route the A2A traffic through &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> for rate limiting, authentication, and observability&lt;/li>
&lt;li>&lt;strong>Build other chat integrations&lt;/strong>: The A2A protocol is platform-agnostic. Swap &lt;code>python-telegram-bot&lt;/code> for &lt;code>discord.py&lt;/code>, &lt;code>slack-bolt&lt;/code>, or even a WhatsApp integration and the A2A layer stays exactly the same&lt;/li>
&lt;/ul>
&lt;p>The pattern works for any chat platform. The A2A protocol is the universal adapter.&lt;/p>
&lt;hr>
&lt;p>&lt;em>The full source code and Kubernetes manifests are in the &lt;a href="https://github.com/ProfessorSeb/k8s-iceman">k8s-iceman&lt;/a> repo under &lt;code>apps/telegram-bot-src/&lt;/code> and &lt;code>manifests/kagent-examples/telegram-bot/&lt;/code>.&lt;/em>&lt;/p></content:encoded></item><item><title>How To: Run agentgateway standalone locally (UI + basic smoke test)</title><link>https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/</link><pubDate>Thu, 12 Mar 2026 08:04:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-12-agentgateway-quickstart-standalone/</guid><description>&lt;h1 id="how-to-run-agentgateway-standalone-locally-ui--basic-smoke-test">How To: Run agentgateway standalone locally (UI + basic smoke test)&lt;/h1>
&lt;p>This is a quick-start for &lt;strong>agentgateway v1.0.0-alpha.4&lt;/strong> in standalone mode.&lt;/p>
&lt;p>Upstream repo: &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>
Latest alpha release notes: &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="option-a--docker-fastest">Option A — Docker (fastest)&lt;/h2>
&lt;p>The project publishes a container image:&lt;/p>
&lt;ul>
&lt;li>&lt;code>cr.agentgateway.dev/agentgateway:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --rm -p 15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.0.0-alpha.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then open:&lt;/p>
&lt;ul>
&lt;li>UI: http://localhost:15000/ui&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="option-b--binary">Option B — Binary&lt;/h2>
&lt;p>The release also publishes binaries (see the GitHub release assets). Download the one for your OS/arch, then run it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then open:&lt;/p>
&lt;ul>
&lt;li>UI: http://localhost:15000/ui&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="what-to-check-first">What to check first&lt;/h2>
&lt;ul>
&lt;li>Does the UI load at &lt;code>/ui&lt;/code>?&lt;/li>
&lt;li>Are there obvious errors in logs on startup?&lt;/li>
&lt;li>Can you see config/state updating when you apply changes?&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="next-step-one-real-use-case">Next step: one real use case&lt;/h2>
&lt;p>Tell me which “first win” you want and I’ll build the smallest working example:&lt;/p>
&lt;ul>
&lt;li>Proxy OpenAI through agentgateway&lt;/li>
&lt;li>Route + observe MCP tool traffic&lt;/li>
&lt;li>A2A connectivity demo&lt;/li>
&lt;/ul></description><content:encoded>&lt;h1 id="how-to-run-agentgateway-standalone-locally-ui--basic-smoke-test">How To: Run agentgateway standalone locally (UI + basic smoke test)&lt;/h1>
&lt;p>This is a quick-start for &lt;strong>agentgateway v1.0.0-alpha.4&lt;/strong> in standalone mode.&lt;/p>
&lt;p>Upstream repo: &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>
Latest alpha release notes: &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="option-a--docker-fastest">Option A — Docker (fastest)&lt;/h2>
&lt;p>The project publishes a container image:&lt;/p>
&lt;ul>
&lt;li>&lt;code>cr.agentgateway.dev/agentgateway:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker run --rm -p 15000:15000 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> cr.agentgateway.dev/agentgateway:v1.0.0-alpha.4
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then open:&lt;/p>
&lt;ul>
&lt;li>UI: http://localhost:15000/ui&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="option-b--binary">Option B — Binary&lt;/h2>
&lt;p>The release also publishes binaries (see the GitHub release assets). Download the one for your OS/arch, then run it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">./agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then open:&lt;/p>
&lt;ul>
&lt;li>UI: http://localhost:15000/ui&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="what-to-check-first">What to check first&lt;/h2>
&lt;ul>
&lt;li>Does the UI load at &lt;code>/ui&lt;/code>?&lt;/li>
&lt;li>Are there obvious errors in logs on startup?&lt;/li>
&lt;li>Can you see config/state updating when you apply changes?&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="next-step-one-real-use-case">Next step: one real use case&lt;/h2>
&lt;p>Tell me which “first win” you want and I’ll build the smallest working example:&lt;/p>
&lt;ul>
&lt;li>Proxy OpenAI through agentgateway&lt;/li>
&lt;li>Route + observe MCP tool traffic&lt;/li>
&lt;li>A2A connectivity demo&lt;/li>
&lt;/ul></content:encoded></item><item><title>How To: Install agentgateway on Kubernetes (Helm OCI)</title><link>https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/</link><pubDate>Thu, 12 Mar 2026 08:02:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-12-agentgateway-quickstart-kubernetes/</guid><description>&lt;h1 id="how-to-install-agentgateway-on-kubernetes-helm-oci">How To: Install agentgateway on Kubernetes (Helm OCI)&lt;/h1>
&lt;p>This is a “get it running fast” install for &lt;strong>agentgateway v1.0.0-alpha.4&lt;/strong> using the OCI Helm charts published by the project.&lt;/p>
&lt;p>Upstream release notes (artifacts list):
&lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4&lt;/a>&lt;/p>
&lt;blockquote>
&lt;p>Note: exact values and defaults can change between alphas; treat this as a baseline and validate in your cluster.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="prereqs">Prereqs&lt;/h2>
&lt;ul>
&lt;li>Kubernetes cluster&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code>&lt;/li>
&lt;li>Helm v3 with OCI support&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="1-pick-a-namespace">1) Pick a namespace&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">AGW_NS&lt;/span>&lt;span class="o">=&lt;/span>agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create namespace &lt;span class="nv">$AGW_NS&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="2-install-crds">2) Install CRDs&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">AGW_VER&lt;/span>&lt;span class="o">=&lt;/span>v1.0.0-alpha.4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install agentgateway-crds oci://cr.agentgateway.dev/charts/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$AGW_VER&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="3-install-agentgateway">3) Install agentgateway&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install agentgateway oci://cr.agentgateway.dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$AGW_VER&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="4-verify-pods">4) Verify pods&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="5-next-configure-your-first-routetool-access">5) Next: configure your first route/tool access&lt;/h2>
&lt;p>At this point agentgateway is installed; the next step is to define how traffic flows (MCP servers, policies, routes, etc.).&lt;/p>
&lt;p>If you tell me your target (OpenAI proxying? MCP multiplexing? A2A?), I’ll write the exact minimal config example as a follow-up.&lt;/p></description><content:encoded>&lt;h1 id="how-to-install-agentgateway-on-kubernetes-helm-oci">How To: Install agentgateway on Kubernetes (Helm OCI)&lt;/h1>
&lt;p>This is a “get it running fast” install for &lt;strong>agentgateway v1.0.0-alpha.4&lt;/strong> using the OCI Helm charts published by the project.&lt;/p>
&lt;p>Upstream release notes (artifacts list):
&lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4&lt;/a>&lt;/p>
&lt;blockquote>
&lt;p>Note: exact values and defaults can change between alphas; treat this as a baseline and validate in your cluster.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="prereqs">Prereqs&lt;/h2>
&lt;ul>
&lt;li>Kubernetes cluster&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code>&lt;/li>
&lt;li>Helm v3 with OCI support&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="1-pick-a-namespace">1) Pick a namespace&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">AGW_NS&lt;/span>&lt;span class="o">=&lt;/span>agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create namespace &lt;span class="nv">$AGW_NS&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="2-install-crds">2) Install CRDs&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">AGW_VER&lt;/span>&lt;span class="o">=&lt;/span>v1.0.0-alpha.4
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install agentgateway-crds oci://cr.agentgateway.dev/charts/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$AGW_VER&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="3-install-agentgateway">3) Install agentgateway&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm install agentgateway oci://cr.agentgateway.dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$AGW_VER&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="4-verify-pods">4) Verify pods&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n &lt;span class="nv">$AGW_NS&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="5-next-configure-your-first-routetool-access">5) Next: configure your first route/tool access&lt;/h2>
&lt;p>At this point agentgateway is installed; the next step is to define how traffic flows (MCP servers, policies, routes, etc.).&lt;/p>
&lt;p>If you tell me your target (OpenAI proxying? MCP multiplexing? A2A?), I’ll write the exact minimal config example as a follow-up.&lt;/p></content:encoded></item><item><title>agentgateway v1.0 (alpha): Why the 1.0 Line Matters (and What Changed)</title><link>https://maniak.io/articles/2026-03-12-agentgateway-v1-why-it-matters/</link><pubDate>Thu, 12 Mar 2026 08:00:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-12-agentgateway-v1-why-it-matters/</guid><description>&lt;h1 id="agentgateway-v10-alpha-why-the-10-line-matters-and-what-changed">agentgateway v1.0 (alpha): Why the 1.0 Line Matters (and What Changed)&lt;/h1>
&lt;p>If you’ve been tracking &lt;strong>agentgateway&lt;/strong>, you probably noticed something big: it crossed into the &lt;strong>v1.0.0&lt;/strong> release line.&lt;/p>
&lt;p>As of today, the latest published release is &lt;strong>&lt;code>v1.0.0-alpha.4&lt;/code>&lt;/strong> — so this isn’t “GA 1.0” yet — but the &lt;strong>1.0 line itself is the story&lt;/strong>.&lt;/p>
&lt;p>This post breaks down why &lt;strong>v1.0&lt;/strong> is significant, what it changes operationally, and how you should think about upgrading if you were previously consuming agentgateway via &lt;strong>kgateway&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="quick-context-what-is-agentgateway">Quick context: what is agentgateway?&lt;/h2>
&lt;p>&lt;strong>agentgateway&lt;/strong> is an open-source &lt;strong>data plane&lt;/strong> optimized for agentic AI connectivity — agent-to-tool and agent-to-agent — with a focus on:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Security and governance&lt;/strong> around tool access (MCP) and agent interoperability (A2A)&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> (including OpenTelemetry)&lt;/li>
&lt;li>&lt;strong>High-performance&lt;/strong> (Rust-based)&lt;/li>
&lt;li>&lt;strong>Run anywhere&lt;/strong> (standalone or Kubernetes)&lt;/li>
&lt;/ul>
&lt;p>Upstream repo: &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="why-the-v10-line-is-significant">Why the v1.0 line is significant&lt;/h2>
&lt;h3 id="1-decoupling-from-kgateway-versioning--lifecycle">1) Decoupling from kgateway (versioning + lifecycle)&lt;/h3>
&lt;p>The most important “why 1.0” detail is &lt;strong>not&lt;/strong> a single feature — it’s &lt;strong>project independence&lt;/strong>.&lt;/p>
&lt;p>From the &lt;code>v1.0.0-alpha.2&lt;/code> release notes:&lt;/p>
&lt;ul>
&lt;li>agentgateway is now &lt;strong>entirely decoupled from kgateway&lt;/strong>&lt;/li>
&lt;li>Kubernetes deployment used to be delivered via kgateway and followed kgateway’s versioning scheme&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>What that fixes in practice:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>You no longer have to explain &lt;em>two&lt;/em> versions for “agentgateway on k8s” (controller vs dataplane)&lt;/li>
&lt;li>Releases become &lt;strong>unified under one repo&lt;/strong> with consistent artifacts&lt;/li>
&lt;/ul>
&lt;h3 id="2-one-release-one-set-of-artifacts">2) One release, one set of artifacts&lt;/h3>
&lt;p>The v1.0.0 alpha releases publish a consistent set of artifacts:&lt;/p>
&lt;ul>
&lt;li>Docker images (controller + gateway)&lt;/li>
&lt;li>Helm charts (agentgateway + agentgateway-crds)&lt;/li>
&lt;li>Binaries&lt;/li>
&lt;/ul>
&lt;p>Example (alpha.4):&lt;/p>
&lt;ul>
&lt;li>&lt;code>cr.agentgateway.dev/agentgateway:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;li>&lt;code>cr.agentgateway.dev/controller:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;li>&lt;code>cr.agentgateway.dev/charts/agentgateway:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;li>&lt;code>cr.agentgateway.dev/charts/agentgateway-crds:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>This is a very “1.0-adjacent” sign: the project is taking packaging/distribution seriously.&lt;/p>
&lt;h3 id="3-the-project-is-signaling-stability-goals">3) The project is signaling stability goals&lt;/h3>
&lt;p>Even though it’s still alpha, moving to v1 is a public signal that the project is:&lt;/p>
&lt;ul>
&lt;li>drawing a line around scope&lt;/li>
&lt;li>solidifying APIs (CRDs, config models)&lt;/li>
&lt;li>standardizing install paths&lt;/li>
&lt;/ul>
&lt;p>In other words: it’s positioning itself to be something you can &lt;strong>operationalize&lt;/strong>, not just demo.&lt;/p>
&lt;hr>
&lt;h2 id="notable-changes-in-the-v100-alpha-series-high-level">Notable changes in the v1.0.0-alpha series (high-level)&lt;/h2>
&lt;p>From the &lt;code>v1.0.0-alpha.4&lt;/code> notes, a few changes that matter operationally:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenTelemetry support for access logs&lt;/strong> (great for real-world production debugging and cost attribution)&lt;/li>
&lt;li>&lt;strong>Gateway API alignment updates&lt;/strong> (example: TLSRoute v1 bump)&lt;/li>
&lt;li>Release artifact / versioning cleanup (consistent &lt;code>v&lt;/code> prefixes)&lt;/li>
&lt;/ul>
&lt;p>Release notes:&lt;/p>
&lt;ul>
&lt;li>alpha.2: &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.2">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.2&lt;/a>&lt;/li>
&lt;li>alpha.4: &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="upgrade--adoption-guidance">Upgrade / adoption guidance&lt;/h2>
&lt;h3 id="if-youre-new">If you’re new&lt;/h3>
&lt;p>Start with v1.0.0-alpha.4 and pick one track:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Standalone&lt;/strong> for local dev and labs&lt;/li>
&lt;li>&lt;strong>Kubernetes&lt;/strong> if you want multi-tenant, policy, and integration with existing cluster operations&lt;/li>
&lt;/ul>
&lt;h3 id="if-you-came-via-kgateway">If you came via kgateway&lt;/h3>
&lt;p>The key shift is that &lt;strong>agentgateway now has its own release line&lt;/strong>, but it can still be used as a supported data plane within kgateway.&lt;/p>
&lt;p>Practical recommendation:&lt;/p>
&lt;ul>
&lt;li>treat v1.0 alpha as &lt;strong>a migration window&lt;/strong>&lt;/li>
&lt;li>stand up v1.0 alpha in parallel, validate config and policy behavior, then cut over&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="whats-next">What’s next&lt;/h2>
&lt;p>In the next posts I’ll publish two quick how-tos:&lt;/p>
&lt;ul>
&lt;li>“Install agentgateway on Kubernetes in 10 minutes”&lt;/li>
&lt;li>“Run agentgateway standalone locally (and what to look at in the UI)”&lt;/li>
&lt;/ul>
&lt;p>If you want one single “do it all” tutorial instead (one post, end-to-end), that also works — it’s just longer.&lt;/p></description><content:encoded>&lt;h1 id="agentgateway-v10-alpha-why-the-10-line-matters-and-what-changed">agentgateway v1.0 (alpha): Why the 1.0 Line Matters (and What Changed)&lt;/h1>
&lt;p>If you’ve been tracking &lt;strong>agentgateway&lt;/strong>, you probably noticed something big: it crossed into the &lt;strong>v1.0.0&lt;/strong> release line.&lt;/p>
&lt;p>As of today, the latest published release is &lt;strong>&lt;code>v1.0.0-alpha.4&lt;/code>&lt;/strong> — so this isn’t “GA 1.0” yet — but the &lt;strong>1.0 line itself is the story&lt;/strong>.&lt;/p>
&lt;p>This post breaks down why &lt;strong>v1.0&lt;/strong> is significant, what it changes operationally, and how you should think about upgrading if you were previously consuming agentgateway via &lt;strong>kgateway&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="quick-context-what-is-agentgateway">Quick context: what is agentgateway?&lt;/h2>
&lt;p>&lt;strong>agentgateway&lt;/strong> is an open-source &lt;strong>data plane&lt;/strong> optimized for agentic AI connectivity — agent-to-tool and agent-to-agent — with a focus on:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Security and governance&lt;/strong> around tool access (MCP) and agent interoperability (A2A)&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> (including OpenTelemetry)&lt;/li>
&lt;li>&lt;strong>High-performance&lt;/strong> (Rust-based)&lt;/li>
&lt;li>&lt;strong>Run anywhere&lt;/strong> (standalone or Kubernetes)&lt;/li>
&lt;/ul>
&lt;p>Upstream repo: &lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="why-the-v10-line-is-significant">Why the v1.0 line is significant&lt;/h2>
&lt;h3 id="1-decoupling-from-kgateway-versioning--lifecycle">1) Decoupling from kgateway (versioning + lifecycle)&lt;/h3>
&lt;p>The most important “why 1.0” detail is &lt;strong>not&lt;/strong> a single feature — it’s &lt;strong>project independence&lt;/strong>.&lt;/p>
&lt;p>From the &lt;code>v1.0.0-alpha.2&lt;/code> release notes:&lt;/p>
&lt;ul>
&lt;li>agentgateway is now &lt;strong>entirely decoupled from kgateway&lt;/strong>&lt;/li>
&lt;li>Kubernetes deployment used to be delivered via kgateway and followed kgateway’s versioning scheme&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>What that fixes in practice:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>You no longer have to explain &lt;em>two&lt;/em> versions for “agentgateway on k8s” (controller vs dataplane)&lt;/li>
&lt;li>Releases become &lt;strong>unified under one repo&lt;/strong> with consistent artifacts&lt;/li>
&lt;/ul>
&lt;h3 id="2-one-release-one-set-of-artifacts">2) One release, one set of artifacts&lt;/h3>
&lt;p>The v1.0.0 alpha releases publish a consistent set of artifacts:&lt;/p>
&lt;ul>
&lt;li>Docker images (controller + gateway)&lt;/li>
&lt;li>Helm charts (agentgateway + agentgateway-crds)&lt;/li>
&lt;li>Binaries&lt;/li>
&lt;/ul>
&lt;p>Example (alpha.4):&lt;/p>
&lt;ul>
&lt;li>&lt;code>cr.agentgateway.dev/agentgateway:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;li>&lt;code>cr.agentgateway.dev/controller:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;li>&lt;code>cr.agentgateway.dev/charts/agentgateway:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;li>&lt;code>cr.agentgateway.dev/charts/agentgateway-crds:v1.0.0-alpha.4&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>This is a very “1.0-adjacent” sign: the project is taking packaging/distribution seriously.&lt;/p>
&lt;h3 id="3-the-project-is-signaling-stability-goals">3) The project is signaling stability goals&lt;/h3>
&lt;p>Even though it’s still alpha, moving to v1 is a public signal that the project is:&lt;/p>
&lt;ul>
&lt;li>drawing a line around scope&lt;/li>
&lt;li>solidifying APIs (CRDs, config models)&lt;/li>
&lt;li>standardizing install paths&lt;/li>
&lt;/ul>
&lt;p>In other words: it’s positioning itself to be something you can &lt;strong>operationalize&lt;/strong>, not just demo.&lt;/p>
&lt;hr>
&lt;h2 id="notable-changes-in-the-v100-alpha-series-high-level">Notable changes in the v1.0.0-alpha series (high-level)&lt;/h2>
&lt;p>From the &lt;code>v1.0.0-alpha.4&lt;/code> notes, a few changes that matter operationally:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenTelemetry support for access logs&lt;/strong> (great for real-world production debugging and cost attribution)&lt;/li>
&lt;li>&lt;strong>Gateway API alignment updates&lt;/strong> (example: TLSRoute v1 bump)&lt;/li>
&lt;li>Release artifact / versioning cleanup (consistent &lt;code>v&lt;/code> prefixes)&lt;/li>
&lt;/ul>
&lt;p>Release notes:&lt;/p>
&lt;ul>
&lt;li>alpha.2: &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.2">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.2&lt;/a>&lt;/li>
&lt;li>alpha.4: &lt;a href="https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4">https://github.com/agentgateway/agentgateway/releases/tag/v1.0.0-alpha.4&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="upgrade--adoption-guidance">Upgrade / adoption guidance&lt;/h2>
&lt;h3 id="if-youre-new">If you’re new&lt;/h3>
&lt;p>Start with v1.0.0-alpha.4 and pick one track:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Standalone&lt;/strong> for local dev and labs&lt;/li>
&lt;li>&lt;strong>Kubernetes&lt;/strong> if you want multi-tenant, policy, and integration with existing cluster operations&lt;/li>
&lt;/ul>
&lt;h3 id="if-you-came-via-kgateway">If you came via kgateway&lt;/h3>
&lt;p>The key shift is that &lt;strong>agentgateway now has its own release line&lt;/strong>, but it can still be used as a supported data plane within kgateway.&lt;/p>
&lt;p>Practical recommendation:&lt;/p>
&lt;ul>
&lt;li>treat v1.0 alpha as &lt;strong>a migration window&lt;/strong>&lt;/li>
&lt;li>stand up v1.0 alpha in parallel, validate config and policy behavior, then cut over&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="whats-next">What’s next&lt;/h2>
&lt;p>In the next posts I’ll publish two quick how-tos:&lt;/p>
&lt;ul>
&lt;li>“Install agentgateway on Kubernetes in 10 minutes”&lt;/li>
&lt;li>“Run agentgateway standalone locally (and what to look at in the UI)”&lt;/li>
&lt;/ul>
&lt;p>If you want one single “do it all” tutorial instead (one post, end-to-end), that also works — it’s just longer.&lt;/p></content:encoded></item><item><title>What is agentgateway.dev? (And why it exists)</title><link>https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/</link><pubDate>Thu, 12 Mar 2026 07:10:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-12-what-is-agentgateway-dev/</guid><description>&lt;h1 id="what-is-agentgatewaydev-and-why-it-exists">What is agentgateway.dev? (And why it exists)&lt;/h1>
&lt;p>If you’re building &lt;strong>agentic AI&lt;/strong> systems, you eventually hit the same wall:&lt;/p>
&lt;ul>
&lt;li>Your agents need &lt;strong>tools&lt;/strong> (MCP servers, internal APIs, SaaS)&lt;/li>
&lt;li>They need to talk to &lt;strong>other agents&lt;/strong> (A2A)&lt;/li>
&lt;li>They need to call &lt;strong>LLM providers&lt;/strong> (often multiple)&lt;/li>
&lt;/ul>
&lt;p>…and suddenly “just use an API gateway” stops being a satisfying answer.&lt;/p>
&lt;p>That’s what &lt;strong>agentgateway.dev&lt;/strong> is about.&lt;/p>
&lt;p>&lt;strong>agentgateway.dev&lt;/strong> is the documentation hub for the open source &lt;strong>agentgateway&lt;/strong> project — a connectivity data plane designed specifically for agent workloads.&lt;/p>
&lt;p>Docs entry point (standalone):
&lt;a href="https://agentgateway.dev/docs/standalone/latest/">https://agentgateway.dev/docs/standalone/latest/&lt;/a>&lt;/p>
&lt;p>Upstream repo:
&lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="why-a-new-gateway-for-agents">Why a new gateway for agents?&lt;/h2>
&lt;p>The docs put it bluntly: &lt;strong>traditional API gateways and reverse proxies aren’t built for MCP and A2A&lt;/strong>.&lt;/p>
&lt;p>Why?&lt;/p>
&lt;p>Traditional gateways assume:&lt;/p>
&lt;ul>
&lt;li>stateless request/response&lt;/li>
&lt;li>one request → one backend&lt;/li>
&lt;li>client-initiated traffic only&lt;/li>
&lt;/ul>
&lt;p>But MCP/A2A introduces:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>stateful JSON-RPC sessions&lt;/strong> with long-lived connections&lt;/li>
&lt;li>&lt;strong>fan-out&lt;/strong> across multiple tool servers&lt;/li>
&lt;li>&lt;strong>server-initiated events&lt;/strong> (SSE) that must route back to the correct client session&lt;/li>
&lt;li>&lt;strong>per-session authorization&lt;/strong> (different users/agents should see different tools)&lt;/li>
&lt;/ul>
&lt;p>In other words: the gateway needs to be protocol-aware and session-aware — not just path-aware.&lt;/p>
&lt;hr>
&lt;h2 id="what-agentgateway-actually-is-in-one-sentence">What agentgateway actually is (in one sentence)&lt;/h2>
&lt;p>Agentgateway is a unified &lt;strong>LLM + MCP + A2A gateway&lt;/strong>, built for:&lt;/p>
&lt;ul>
&lt;li>enterprise-grade security&lt;/li>
&lt;li>observability&lt;/li>
&lt;li>resiliency&lt;/li>
&lt;li>multi-tenancy&lt;/li>
&lt;/ul>
&lt;p>And it’s implemented in &lt;strong>Rust&lt;/strong> for performance and memory safety (important for long-lived, high-concurrency sessions).&lt;/p>
&lt;hr>
&lt;h2 id="the-three-big-product-pillars">The three big product pillars&lt;/h2>
&lt;h3 id="1-llm-gateway">1) LLM Gateway&lt;/h3>
&lt;p>Agentgateway can route traffic to major LLM providers behind a &lt;strong>unified OpenAI-compatible API&lt;/strong>, so you can switch providers without rewriting your application.&lt;/p>
&lt;p>The docs also call out “native vs translation” support per provider/API, which is a real-world detail that matters when fields/models evolve quickly.&lt;/p>
&lt;h3 id="2-mcp-gateway">2) MCP Gateway&lt;/h3>
&lt;p>Agentgateway’s MCP features (from the docs):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tool federation&lt;/strong>: aggregate multiple MCP servers behind a single endpoint&lt;/li>
&lt;li>Multiple transports: stdio, HTTP/SSE, Streamable HTTP&lt;/li>
&lt;li>&lt;strong>OpenAPI integration&lt;/strong>: expose existing REST APIs as MCP tools&lt;/li>
&lt;li>AuthN/AuthZ: MCP auth spec compliance + OAuth providers (Auth0, Keycloak)&lt;/li>
&lt;/ul>
&lt;p>This is where agentgateway becomes the “connective tissue” between LLMs and tools.&lt;/p>
&lt;h3 id="3-a2a-gateway">3) A2A Gateway&lt;/h3>
&lt;p>Agentgateway also supports the Agent-to-Agent (A2A) protocol so agents can:&lt;/p>
&lt;ul>
&lt;li>discover each other’s capabilities&lt;/li>
&lt;li>negotiate modalities (text/forms/media)&lt;/li>
&lt;li>collaborate on long-running tasks&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="security--observability-the-part-that-turns-demos-into-systems">Security &amp;amp; observability (the part that turns demos into systems)&lt;/h2>
&lt;p>From the docs:&lt;/p>
&lt;ul>
&lt;li>Authentication: JWT, API keys, basic auth, MCP auth spec&lt;/li>
&lt;li>Authorization: fine-grained RBAC with the &lt;strong>Cedar policy engine&lt;/strong>&lt;/li>
&lt;li>Traffic policies: rate limiting, CORS, TLS, external authz&lt;/li>
&lt;li>Observability: built-in OpenTelemetry metrics/logs/tracing&lt;/li>
&lt;/ul>
&lt;p>If you’re building agent systems you expect to run for months, this is the difference between:&lt;/p>
&lt;ul>
&lt;li>“we tested it once”&lt;/li>
&lt;li>and “we can operate it safely”&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="why-now-the-timing">Why now (the timing)&lt;/h2>
&lt;p>The timing is interesting because multiple milestones are converging:&lt;/p>
&lt;ul>
&lt;li>The repo is about &lt;strong>1 year old&lt;/strong> (created March 2025)&lt;/li>
&lt;li>It’s &lt;strong>nearing ~2k GitHub stars&lt;/strong>&lt;/li>
&lt;li>The project is crossing into the &lt;strong>v1.0 release line&lt;/strong> (currently in alpha)&lt;/li>
&lt;/ul>
&lt;p>This is usually the moment when a project shifts from “early adopter playground” to “this is becoming a platform.”&lt;/p>
&lt;hr>
&lt;h2 id="next-what-id-build-first">Next: what I’d build first&lt;/h2>
&lt;p>If you want a practical entry point, pick one:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>LLM routing + observability&lt;/strong> (single endpoint, multiple providers)&lt;/li>
&lt;li>&lt;strong>MCP tool federation&lt;/strong> (one tool endpoint, many MCP servers)&lt;/li>
&lt;li>&lt;strong>Policy + RBAC&lt;/strong> (who can call which tools, under what conditions)&lt;/li>
&lt;/ol>
&lt;p>If you tell me which one you want to lead with, I’ll write a follow-up tutorial that’s copy/paste runnable.&lt;/p></description><content:encoded>&lt;h1 id="what-is-agentgatewaydev-and-why-it-exists">What is agentgateway.dev? (And why it exists)&lt;/h1>
&lt;p>If you’re building &lt;strong>agentic AI&lt;/strong> systems, you eventually hit the same wall:&lt;/p>
&lt;ul>
&lt;li>Your agents need &lt;strong>tools&lt;/strong> (MCP servers, internal APIs, SaaS)&lt;/li>
&lt;li>They need to talk to &lt;strong>other agents&lt;/strong> (A2A)&lt;/li>
&lt;li>They need to call &lt;strong>LLM providers&lt;/strong> (often multiple)&lt;/li>
&lt;/ul>
&lt;p>…and suddenly “just use an API gateway” stops being a satisfying answer.&lt;/p>
&lt;p>That’s what &lt;strong>agentgateway.dev&lt;/strong> is about.&lt;/p>
&lt;p>&lt;strong>agentgateway.dev&lt;/strong> is the documentation hub for the open source &lt;strong>agentgateway&lt;/strong> project — a connectivity data plane designed specifically for agent workloads.&lt;/p>
&lt;p>Docs entry point (standalone):
&lt;a href="https://agentgateway.dev/docs/standalone/latest/">https://agentgateway.dev/docs/standalone/latest/&lt;/a>&lt;/p>
&lt;p>Upstream repo:
&lt;a href="https://github.com/agentgateway/agentgateway">https://github.com/agentgateway/agentgateway&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="why-a-new-gateway-for-agents">Why a new gateway for agents?&lt;/h2>
&lt;p>The docs put it bluntly: &lt;strong>traditional API gateways and reverse proxies aren’t built for MCP and A2A&lt;/strong>.&lt;/p>
&lt;p>Why?&lt;/p>
&lt;p>Traditional gateways assume:&lt;/p>
&lt;ul>
&lt;li>stateless request/response&lt;/li>
&lt;li>one request → one backend&lt;/li>
&lt;li>client-initiated traffic only&lt;/li>
&lt;/ul>
&lt;p>But MCP/A2A introduces:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>stateful JSON-RPC sessions&lt;/strong> with long-lived connections&lt;/li>
&lt;li>&lt;strong>fan-out&lt;/strong> across multiple tool servers&lt;/li>
&lt;li>&lt;strong>server-initiated events&lt;/strong> (SSE) that must route back to the correct client session&lt;/li>
&lt;li>&lt;strong>per-session authorization&lt;/strong> (different users/agents should see different tools)&lt;/li>
&lt;/ul>
&lt;p>In other words: the gateway needs to be protocol-aware and session-aware — not just path-aware.&lt;/p>
&lt;hr>
&lt;h2 id="what-agentgateway-actually-is-in-one-sentence">What agentgateway actually is (in one sentence)&lt;/h2>
&lt;p>Agentgateway is a unified &lt;strong>LLM + MCP + A2A gateway&lt;/strong>, built for:&lt;/p>
&lt;ul>
&lt;li>enterprise-grade security&lt;/li>
&lt;li>observability&lt;/li>
&lt;li>resiliency&lt;/li>
&lt;li>multi-tenancy&lt;/li>
&lt;/ul>
&lt;p>And it’s implemented in &lt;strong>Rust&lt;/strong> for performance and memory safety (important for long-lived, high-concurrency sessions).&lt;/p>
&lt;hr>
&lt;h2 id="the-three-big-product-pillars">The three big product pillars&lt;/h2>
&lt;h3 id="1-llm-gateway">1) LLM Gateway&lt;/h3>
&lt;p>Agentgateway can route traffic to major LLM providers behind a &lt;strong>unified OpenAI-compatible API&lt;/strong>, so you can switch providers without rewriting your application.&lt;/p>
&lt;p>The docs also call out “native vs translation” support per provider/API, which is a real-world detail that matters when fields/models evolve quickly.&lt;/p>
&lt;h3 id="2-mcp-gateway">2) MCP Gateway&lt;/h3>
&lt;p>Agentgateway’s MCP features (from the docs):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tool federation&lt;/strong>: aggregate multiple MCP servers behind a single endpoint&lt;/li>
&lt;li>Multiple transports: stdio, HTTP/SSE, Streamable HTTP&lt;/li>
&lt;li>&lt;strong>OpenAPI integration&lt;/strong>: expose existing REST APIs as MCP tools&lt;/li>
&lt;li>AuthN/AuthZ: MCP auth spec compliance + OAuth providers (Auth0, Keycloak)&lt;/li>
&lt;/ul>
&lt;p>This is where agentgateway becomes the “connective tissue” between LLMs and tools.&lt;/p>
&lt;h3 id="3-a2a-gateway">3) A2A Gateway&lt;/h3>
&lt;p>Agentgateway also supports the Agent-to-Agent (A2A) protocol so agents can:&lt;/p>
&lt;ul>
&lt;li>discover each other’s capabilities&lt;/li>
&lt;li>negotiate modalities (text/forms/media)&lt;/li>
&lt;li>collaborate on long-running tasks&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="security--observability-the-part-that-turns-demos-into-systems">Security &amp;amp; observability (the part that turns demos into systems)&lt;/h2>
&lt;p>From the docs:&lt;/p>
&lt;ul>
&lt;li>Authentication: JWT, API keys, basic auth, MCP auth spec&lt;/li>
&lt;li>Authorization: fine-grained RBAC with the &lt;strong>Cedar policy engine&lt;/strong>&lt;/li>
&lt;li>Traffic policies: rate limiting, CORS, TLS, external authz&lt;/li>
&lt;li>Observability: built-in OpenTelemetry metrics/logs/tracing&lt;/li>
&lt;/ul>
&lt;p>If you’re building agent systems you expect to run for months, this is the difference between:&lt;/p>
&lt;ul>
&lt;li>“we tested it once”&lt;/li>
&lt;li>and “we can operate it safely”&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="why-now-the-timing">Why now (the timing)&lt;/h2>
&lt;p>The timing is interesting because multiple milestones are converging:&lt;/p>
&lt;ul>
&lt;li>The repo is about &lt;strong>1 year old&lt;/strong> (created March 2025)&lt;/li>
&lt;li>It’s &lt;strong>nearing ~2k GitHub stars&lt;/strong>&lt;/li>
&lt;li>The project is crossing into the &lt;strong>v1.0 release line&lt;/strong> (currently in alpha)&lt;/li>
&lt;/ul>
&lt;p>This is usually the moment when a project shifts from “early adopter playground” to “this is becoming a platform.”&lt;/p>
&lt;hr>
&lt;h2 id="next-what-id-build-first">Next: what I’d build first&lt;/h2>
&lt;p>If you want a practical entry point, pick one:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>LLM routing + observability&lt;/strong> (single endpoint, multiple providers)&lt;/li>
&lt;li>&lt;strong>MCP tool federation&lt;/strong> (one tool endpoint, many MCP servers)&lt;/li>
&lt;li>&lt;strong>Policy + RBAC&lt;/strong> (who can call which tools, under what conditions)&lt;/li>
&lt;/ol>
&lt;p>If you tell me which one you want to lead with, I’ll write a follow-up tutorial that’s copy/paste runnable.&lt;/p></content:encoded></item><item><title>Human-in-the-Loop: Building AI Agents You Can Actually Trust on Kubernetes</title><link>https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/</link><pubDate>Wed, 11 Mar 2026 10:34:00 -0400</pubDate><guid>https://maniak.io/articles/2026-03-11-human-in-the-loop-kagent/</guid><description>&lt;h1 id="human-in-the-loop-building-ai-agents-you-can-actually-trust-on-kubernetes">Human-in-the-Loop: Building AI Agents You Can Actually Trust on Kubernetes&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>AI agents are getting good — really good — at taking autonomous action. They can inspect your Kubernetes cluster, diagnose problems, create resources, and even clean up after themselves. But here&amp;rsquo;s the uncomfortable question most teams are quietly asking:&lt;/p>
&lt;p>&lt;em>&amp;ldquo;Do I actually want an AI agent deleting things in my production cluster without asking me first?&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The answer, almost universally, is no.&lt;/p>
&lt;p>This is where &lt;strong>Human-in-the-Loop (HITL)&lt;/strong> comes in. It&amp;rsquo;s the pattern that lets you give an AI agent real tools and real capabilities, while keeping a human in the decision loop for anything that matters. Not everything — just the operations where a mistake could ruin your afternoon.&lt;/p>
&lt;p>In this post, I&amp;rsquo;ll walk through how HITL works in &lt;a href="https://kagent.dev">kagent&lt;/a>, an open-source project for building Kubernetes-native AI agents. We&amp;rsquo;ll go from concept to a working agent that pauses, asks for approval, and respects your answer — all running on a local Kind cluster.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem-with-fully-autonomous-agents">The Problem with Fully Autonomous Agents&lt;/h2>
&lt;p>Let&amp;rsquo;s set the scene. You build an AI agent that can manage Kubernetes resources. You give it tools to list pods, apply manifests, delete deployments. It works great in a demo.&lt;/p>
&lt;p>Then someone on your team asks it to &amp;ldquo;clean up the old staging resources&amp;rdquo; and it deletes the wrong namespace. Or it misinterprets &amp;ldquo;scale down the database&amp;rdquo; and sets replicas to zero on your production PostgreSQL StatefulSet.&lt;/p>
&lt;p>The issue isn&amp;rsquo;t that the agent is dumb — it&amp;rsquo;s that there was no checkpoint between &amp;ldquo;the agent decided to do something&amp;rdquo; and &amp;ldquo;the thing got done.&amp;rdquo; There was no moment where a human could look at the plan and say, &amp;ldquo;Wait, no. Not that.&amp;rdquo;&lt;/p>
&lt;p>Fully autonomous agents are fine for read-only operations. For anything that changes state, you need a gate.&lt;/p>
&lt;hr>
&lt;h2 id="what-human-in-the-loop-actually-means">What Human-in-the-Loop Actually Means&lt;/h2>
&lt;p>HITL isn&amp;rsquo;t a single feature — it&amp;rsquo;s a design pattern with a few distinct mechanisms:&lt;/p>
&lt;h3 id="1-tool-approval">1. Tool Approval&lt;/h3>
&lt;p>The agent decides to call a tool (like deleting a resource), but instead of executing immediately, the system &lt;strong>pauses&lt;/strong> and presents the action to a human. The human reviews it and either:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Approves&lt;/strong> — the tool executes normally&lt;/li>
&lt;li>&lt;strong>Rejects&lt;/strong> — the agent receives the rejection (with an optional reason) and adapts its approach&lt;/li>
&lt;/ul>
&lt;p>This is the core HITL mechanism. It&amp;rsquo;s simple, explicit, and gives you exactly the control you need.&lt;/p>
&lt;h3 id="2-ask-user">2. Ask User&lt;/h3>
&lt;p>Sometimes the agent doesn&amp;rsquo;t need approval — it needs &lt;strong>information&lt;/strong>. The request is ambiguous, or there are multiple valid paths, and the agent needs you to make a choice.&lt;/p>
&lt;p>For example:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;Which namespace should I create this in?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;Do you want to use PostgreSQL or MySQL?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;Should I apply this to staging or production?&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>The &lt;code>ask_user&lt;/code> tool lets the agent pause execution, ask a question, and wait for your answer before continuing. It&amp;rsquo;s not a safety gate — it&amp;rsquo;s a collaboration mechanism.&lt;/p>
&lt;h3 id="3-the-combination">3. The Combination&lt;/h3>
&lt;p>The real power is when both mechanisms work together. An agent that can:&lt;/p>
&lt;ul>
&lt;li>Read your cluster freely (no interruptions)&lt;/li>
&lt;li>Ask you for clarification when your request is vague&lt;/li>
&lt;li>Show you exactly what it&amp;rsquo;s about to change and wait for your approval&lt;/li>
&lt;li>Accept your rejection gracefully and try a different approach&lt;/li>
&lt;/ul>
&lt;p>That&amp;rsquo;s an agent you can actually put in front of a team.&lt;/p>
&lt;hr>
&lt;h2 id="how-kagent-implements-hitl">How kagent Implements HITL&lt;/h2>
&lt;p>&lt;a href="https://kagent.dev">kagent&lt;/a> is an open-source project for building AI agents that run natively on Kubernetes. Agents are defined as Custom Resources (CRDs), tools are served via MCP (Model Context Protocol) servers, and everything runs inside your cluster.&lt;/p>
&lt;p>HITL in kagent works through two mechanisms, both built on the same underlying infrastructure:&lt;/p>
&lt;h3 id="the-requireapproval-field">The &lt;code>requireApproval&lt;/code> Field&lt;/h3>
&lt;p>When you define an agent, you list the tools it can use. For any tool that should require human approval, you add it to the &lt;code>requireApproval&lt;/code> list:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># read-only, runs freely&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># read-only, runs freely&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># destructive, needs approval&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># destructive, needs approval&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Two lines to add a safety gate. The tools in &lt;code>requireApproval&lt;/code> must also appear in &lt;code>toolNames&lt;/code> — the CRD validates this at creation time, so you can&amp;rsquo;t accidentally gate a tool that isn&amp;rsquo;t assigned to the agent.&lt;/p>
&lt;h3 id="the-ask_user-tool">The &lt;code>ask_user&lt;/code> Tool&lt;/h3>
&lt;p>Every kagent agent automatically has access to &lt;code>ask_user&lt;/code>. You don&amp;rsquo;t need to configure it — it&amp;rsquo;s built-in. When the agent encounters an ambiguous request, it can call &lt;code>ask_user&lt;/code> to pause and ask the human a question.&lt;/p>
&lt;p>The question can be:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Free-text&lt;/strong> — the user types their answer&lt;/li>
&lt;li>&lt;strong>Single-select&lt;/strong> — the user picks from a list of choices&lt;/li>
&lt;li>&lt;strong>Multi-select&lt;/strong> — the user picks multiple options&lt;/li>
&lt;/ul>
&lt;p>The agent gets back structured data, not just a raw string, which makes it easier to act on the answer.&lt;/p>
&lt;h3 id="under-the-hood">Under the Hood&lt;/h3>
&lt;p>Both mechanisms share the same infrastructure:&lt;/p>
&lt;ol>
&lt;li>The agent calls a tool that requires confirmation (either via &lt;code>requireApproval&lt;/code> or &lt;code>ask_user&lt;/code>)&lt;/li>
&lt;li>The executor calls &lt;code>request_confirmation()&lt;/code> and &lt;strong>blocks&lt;/strong>&lt;/li>
&lt;li>An event is generated and sent to the UI via A2A (Agent-to-Agent protocol)&lt;/li>
&lt;li>The UI renders the approval controls or question&lt;/li>
&lt;li>The human responds&lt;/li>
&lt;li>Their response is sent back as an A2A message&lt;/li>
&lt;li>The executor unblocks and the agent continues&lt;/li>
&lt;/ol>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-03-11-human-in-the-loop-kagent/hitl-flow.jpg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-03-11-human-in-the-loop-kagent/hitl-flow.jpg" alt="Human-in-the-Loop flow: request, approval, execution, and feedback." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>HITL flow: the agent proposes actions, a human approves/edits/rejects, and results feed back into the agent context.&lt;/em>&lt;/p>
&lt;p>This shared architecture means HITL is not a bolted-on feature — it&amp;rsquo;s a core part of how kagent agents communicate. Adding approval to a new tool is a one-line YAML change, not a code change.&lt;/p>
&lt;hr>
&lt;h2 id="building-a-hitl-agent-step-by-step">Building a HITL Agent: Step by Step&lt;/h2>
&lt;p>Let&amp;rsquo;s build one. We&amp;rsquo;ll create an agent with access to Kubernetes tools — read-only tools run freely, destructive tools require approval.&lt;/p>
&lt;h3 id="prerequisites">Prerequisites&lt;/h3>
&lt;ul>
&lt;li>Docker, Kind, kubectl, Helm installed&lt;/li>
&lt;li>An OpenAI API key&lt;/li>
&lt;/ul>
&lt;h3 id="1-create-a-cluster">1. Create a Cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name kagent-hitl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-install-kagent">2. Install kagent&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install kagent-crds oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set your API key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install kagent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install kagent oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKey&lt;span class="o">=&lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for pods&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>ready pod --all -n kagent --timeout&lt;span class="o">=&lt;/span>120s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-define-the-agent">3. Define the Agent&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hitl-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">A Kubernetes agent with human-in-the-loop approval for destructive operations.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are a Kubernetes management agent. You help users inspect and manage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> resources in the cluster. Before making any changes, explain what you
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> plan to do. If the user&amp;#39;s request is ambiguous, use the ask_user tool
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> to clarify before proceeding.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_pod_logs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resource_yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_patch_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_patch_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create -f hitl-agent.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="4-open-the-ui">4. Open the UI&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n kagent svc/kagent-ui 8080:8080
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;a href="http://localhost:8080">http://localhost:8080&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="seeing-it-in-action">Seeing It in Action&lt;/h2>
&lt;h3 id="scenario-1-reading-is-free">Scenario 1: Reading is Free&lt;/h3>
&lt;p>You ask the agent: &lt;em>&amp;ldquo;List all pods in the kagent namespace.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent calls &lt;code>k8s_get_resources&lt;/code>. Since this tool is &lt;strong>not&lt;/strong> in &lt;code>requireApproval&lt;/code>, it runs immediately. You see the results with no interruption.&lt;/p>
&lt;p>This is the right UX. You don&amp;rsquo;t want to click &amp;ldquo;Approve&amp;rdquo; every time you ask the agent to read something. Only destructive operations should have gates.&lt;/p>
&lt;h3 id="scenario-2-writes-need-approval">Scenario 2: Writes Need Approval&lt;/h3>
&lt;p>You ask: &lt;em>&amp;ldquo;Create a ConfigMap called test-config in the default namespace with the key message set to &amp;lsquo;hello from kagent&amp;rsquo;.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent calls &lt;code>k8s_apply_manifest&lt;/code>. Since this tool &lt;strong>is&lt;/strong> in &lt;code>requireApproval&lt;/code>, execution pauses. In the UI, you see:&lt;/p>
&lt;ul>
&lt;li>The tool the agent wants to call&lt;/li>
&lt;li>The arguments it wants to pass (including the full YAML manifest)&lt;/li>
&lt;li>&lt;strong>Approve&lt;/strong> and &lt;strong>Reject&lt;/strong> buttons&lt;/li>
&lt;/ul>
&lt;p>You review the manifest. It looks correct. You click &lt;strong>Approve&lt;/strong>. The agent applies the ConfigMap and confirms success.&lt;/p>
&lt;h3 id="scenario-3-rejection-with-reason">Scenario 3: Rejection with Reason&lt;/h3>
&lt;p>You ask: &lt;em>&amp;ldquo;Delete the ConfigMap test-config in the default namespace.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent calls &lt;code>k8s_delete_resource&lt;/code>. Execution pauses. But this time, you click &lt;strong>Reject&lt;/strong> and enter a reason: &lt;em>&amp;ldquo;I want to keep this ConfigMap for now.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent receives your rejection reason. Instead of failing or retrying, it acknowledges your decision and responds: &lt;em>&amp;ldquo;Understood — I&amp;rsquo;ll leave the ConfigMap in place. Let me know if you change your mind.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>This is a key detail. The rejection reason isn&amp;rsquo;t just logged — it&amp;rsquo;s sent back to the LLM as context. The agent can adapt its behavior based on &lt;strong>why&lt;/strong> you said no.&lt;/p>
&lt;h3 id="scenario-4-agent-asks-for-clarification">Scenario 4: Agent Asks for Clarification&lt;/h3>
&lt;p>You ask: &lt;em>&amp;ldquo;Set up a namespace for my application.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent doesn&amp;rsquo;t know what to call the namespace. Instead of guessing, it calls &lt;code>ask_user&lt;/code>: &lt;em>&amp;ldquo;What should the namespace be called?&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>You type &amp;ldquo;staging-app&amp;rdquo; and the agent continues, creating the namespace with the name you specified.&lt;/p>
&lt;p>This is the difference between an agent that guesses wrong and an agent that gets it right. The cost of asking is a few seconds. The cost of guessing wrong is rolling back a mistake.&lt;/p>
&lt;hr>
&lt;h2 id="design-principles">Design Principles&lt;/h2>
&lt;p>A few things about kagent&amp;rsquo;s HITL implementation that I think are worth highlighting:&lt;/p>
&lt;h3 id="1-opt-in-not-opt-out">1. Opt-In, Not Opt-Out&lt;/h3>
&lt;p>Tools run freely by default. You explicitly list which ones need approval. This means adding a new read-only tool to an agent doesn&amp;rsquo;t require updating any approval configuration — it just works.&lt;/p>
&lt;h3 id="2-declarative-not-procedural">2. Declarative, Not Procedural&lt;/h3>
&lt;p>You don&amp;rsquo;t write approval logic in code. You add a tool name to a YAML list. This keeps agent definitions readable and auditable. A security reviewer can look at the agent YAML and immediately see which tools require human approval.&lt;/p>
&lt;h3 id="3-rejection-is-contextual">3. Rejection Is Contextual&lt;/h3>
&lt;p>When you reject a tool call, you can provide a reason. That reason is returned to the LLM as part of the conversation context. This means the agent can learn from your rejection in the current conversation and adjust its approach.&lt;/p>
&lt;h3 id="4-ask_user-is-always-available">4. ask_user Is Always Available&lt;/h3>
&lt;p>You don&amp;rsquo;t need to configure &lt;code>ask_user&lt;/code>. It&amp;rsquo;s built-in on every agent. This encourages agent builders to write system prompts that tell the agent to ask when things are ambiguous, rather than assuming.&lt;/p>
&lt;hr>
&lt;h2 id="when-to-use-hitl-and-when-not-to">When to Use HITL (and When Not To)&lt;/h2>
&lt;p>&lt;strong>Use HITL for:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Any operation that changes cluster state (create, update, delete)&lt;/li>
&lt;li>Operations that are hard to reverse (deleting PVCs, removing finalizers)&lt;/li>
&lt;li>Multi-step workflows where early decisions affect later steps&lt;/li>
&lt;li>Any agent that will be used by people who aren&amp;rsquo;t the agent&amp;rsquo;s author&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Skip HITL for:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Read-only operations (listing, describing, reading logs)&lt;/li>
&lt;li>Internal agent reasoning (tool calls that don&amp;rsquo;t affect external state)&lt;/li>
&lt;li>Fully automated pipelines where latency matters and rollback is cheap&lt;/li>
&lt;/ul>
&lt;p>The goal isn&amp;rsquo;t to make every action require approval — it&amp;rsquo;s to put gates where they matter.&lt;/p>
&lt;hr>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name kagent-hitl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;p>HITL is one piece of the puzzle. kagent also supports:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>A2A protocol&lt;/strong> for agent-to-agent communication&lt;/li>
&lt;li>&lt;strong>MCP servers&lt;/strong> for extending agent capabilities with custom tools&lt;/li>
&lt;li>&lt;strong>Memory&lt;/strong> for agents that learn from past interactions&lt;/li>
&lt;li>&lt;strong>Skills&lt;/strong> for packaging reusable agent behaviors&lt;/li>
&lt;/ul>
&lt;p>If you want to try it yourself, the upstream Episode 03 material is here:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/solo-io/allthings/tree/main/episode-03">https://github.com/solo-io/allthings/tree/main/episode-03&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>kagent is open source: &lt;a href="https://github.com/kagent-dev/kagent">https://github.com/kagent-dev/kagent&lt;/a>&lt;/p>
&lt;hr>
&lt;p>&lt;em>This post is part of the &amp;ldquo;All Things&amp;rdquo; series exploring AI agents on Kubernetes.&lt;/em>&lt;/p></description><content:encoded>&lt;h1 id="human-in-the-loop-building-ai-agents-you-can-actually-trust-on-kubernetes">Human-in-the-Loop: Building AI Agents You Can Actually Trust on Kubernetes&lt;/h1>
&lt;p>&lt;strong>By Sebastian Maniak&lt;/strong>&lt;/p>
&lt;p>AI agents are getting good — really good — at taking autonomous action. They can inspect your Kubernetes cluster, diagnose problems, create resources, and even clean up after themselves. But here&amp;rsquo;s the uncomfortable question most teams are quietly asking:&lt;/p>
&lt;p>&lt;em>&amp;ldquo;Do I actually want an AI agent deleting things in my production cluster without asking me first?&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The answer, almost universally, is no.&lt;/p>
&lt;p>This is where &lt;strong>Human-in-the-Loop (HITL)&lt;/strong> comes in. It&amp;rsquo;s the pattern that lets you give an AI agent real tools and real capabilities, while keeping a human in the decision loop for anything that matters. Not everything — just the operations where a mistake could ruin your afternoon.&lt;/p>
&lt;p>In this post, I&amp;rsquo;ll walk through how HITL works in &lt;a href="https://kagent.dev">kagent&lt;/a>, an open-source project for building Kubernetes-native AI agents. We&amp;rsquo;ll go from concept to a working agent that pauses, asks for approval, and respects your answer — all running on a local Kind cluster.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem-with-fully-autonomous-agents">The Problem with Fully Autonomous Agents&lt;/h2>
&lt;p>Let&amp;rsquo;s set the scene. You build an AI agent that can manage Kubernetes resources. You give it tools to list pods, apply manifests, delete deployments. It works great in a demo.&lt;/p>
&lt;p>Then someone on your team asks it to &amp;ldquo;clean up the old staging resources&amp;rdquo; and it deletes the wrong namespace. Or it misinterprets &amp;ldquo;scale down the database&amp;rdquo; and sets replicas to zero on your production PostgreSQL StatefulSet.&lt;/p>
&lt;p>The issue isn&amp;rsquo;t that the agent is dumb — it&amp;rsquo;s that there was no checkpoint between &amp;ldquo;the agent decided to do something&amp;rdquo; and &amp;ldquo;the thing got done.&amp;rdquo; There was no moment where a human could look at the plan and say, &amp;ldquo;Wait, no. Not that.&amp;rdquo;&lt;/p>
&lt;p>Fully autonomous agents are fine for read-only operations. For anything that changes state, you need a gate.&lt;/p>
&lt;hr>
&lt;h2 id="what-human-in-the-loop-actually-means">What Human-in-the-Loop Actually Means&lt;/h2>
&lt;p>HITL isn&amp;rsquo;t a single feature — it&amp;rsquo;s a design pattern with a few distinct mechanisms:&lt;/p>
&lt;h3 id="1-tool-approval">1. Tool Approval&lt;/h3>
&lt;p>The agent decides to call a tool (like deleting a resource), but instead of executing immediately, the system &lt;strong>pauses&lt;/strong> and presents the action to a human. The human reviews it and either:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Approves&lt;/strong> — the tool executes normally&lt;/li>
&lt;li>&lt;strong>Rejects&lt;/strong> — the agent receives the rejection (with an optional reason) and adapts its approach&lt;/li>
&lt;/ul>
&lt;p>This is the core HITL mechanism. It&amp;rsquo;s simple, explicit, and gives you exactly the control you need.&lt;/p>
&lt;h3 id="2-ask-user">2. Ask User&lt;/h3>
&lt;p>Sometimes the agent doesn&amp;rsquo;t need approval — it needs &lt;strong>information&lt;/strong>. The request is ambiguous, or there are multiple valid paths, and the agent needs you to make a choice.&lt;/p>
&lt;p>For example:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;Which namespace should I create this in?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;Do you want to use PostgreSQL or MySQL?&amp;rdquo;&lt;/li>
&lt;li>&amp;ldquo;Should I apply this to staging or production?&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>The &lt;code>ask_user&lt;/code> tool lets the agent pause execution, ask a question, and wait for your answer before continuing. It&amp;rsquo;s not a safety gate — it&amp;rsquo;s a collaboration mechanism.&lt;/p>
&lt;h3 id="3-the-combination">3. The Combination&lt;/h3>
&lt;p>The real power is when both mechanisms work together. An agent that can:&lt;/p>
&lt;ul>
&lt;li>Read your cluster freely (no interruptions)&lt;/li>
&lt;li>Ask you for clarification when your request is vague&lt;/li>
&lt;li>Show you exactly what it&amp;rsquo;s about to change and wait for your approval&lt;/li>
&lt;li>Accept your rejection gracefully and try a different approach&lt;/li>
&lt;/ul>
&lt;p>That&amp;rsquo;s an agent you can actually put in front of a team.&lt;/p>
&lt;hr>
&lt;h2 id="how-kagent-implements-hitl">How kagent Implements HITL&lt;/h2>
&lt;p>&lt;a href="https://kagent.dev">kagent&lt;/a> is an open-source project for building AI agents that run natively on Kubernetes. Agents are defined as Custom Resources (CRDs), tools are served via MCP (Model Context Protocol) servers, and everything runs inside your cluster.&lt;/p>
&lt;p>HITL in kagent works through two mechanisms, both built on the same underlying infrastructure:&lt;/p>
&lt;h3 id="the-requireapproval-field">The &lt;code>requireApproval&lt;/code> Field&lt;/h3>
&lt;p>When you define an agent, you list the tools it can use. For any tool that should require human approval, you add it to the &lt;code>requireApproval&lt;/code> list:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># read-only, runs freely&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># read-only, runs freely&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># destructive, needs approval&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># destructive, needs approval&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Two lines to add a safety gate. The tools in &lt;code>requireApproval&lt;/code> must also appear in &lt;code>toolNames&lt;/code> — the CRD validates this at creation time, so you can&amp;rsquo;t accidentally gate a tool that isn&amp;rsquo;t assigned to the agent.&lt;/p>
&lt;h3 id="the-ask_user-tool">The &lt;code>ask_user&lt;/code> Tool&lt;/h3>
&lt;p>Every kagent agent automatically has access to &lt;code>ask_user&lt;/code>. You don&amp;rsquo;t need to configure it — it&amp;rsquo;s built-in. When the agent encounters an ambiguous request, it can call &lt;code>ask_user&lt;/code> to pause and ask the human a question.&lt;/p>
&lt;p>The question can be:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Free-text&lt;/strong> — the user types their answer&lt;/li>
&lt;li>&lt;strong>Single-select&lt;/strong> — the user picks from a list of choices&lt;/li>
&lt;li>&lt;strong>Multi-select&lt;/strong> — the user picks multiple options&lt;/li>
&lt;/ul>
&lt;p>The agent gets back structured data, not just a raw string, which makes it easier to act on the answer.&lt;/p>
&lt;h3 id="under-the-hood">Under the Hood&lt;/h3>
&lt;p>Both mechanisms share the same infrastructure:&lt;/p>
&lt;ol>
&lt;li>The agent calls a tool that requires confirmation (either via &lt;code>requireApproval&lt;/code> or &lt;code>ask_user&lt;/code>)&lt;/li>
&lt;li>The executor calls &lt;code>request_confirmation()&lt;/code> and &lt;strong>blocks&lt;/strong>&lt;/li>
&lt;li>An event is generated and sent to the UI via A2A (Agent-to-Agent protocol)&lt;/li>
&lt;li>The UI renders the approval controls or question&lt;/li>
&lt;li>The human responds&lt;/li>
&lt;li>Their response is sent back as an A2A message&lt;/li>
&lt;li>The executor unblocks and the agent continues&lt;/li>
&lt;/ol>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/images/articles/2026-03-11-human-in-the-loop-kagent/hitl-flow.jpg" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/images/articles/2026-03-11-human-in-the-loop-kagent/hitl-flow.jpg" alt="Human-in-the-Loop flow: request, approval, execution, and feedback." loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;em>HITL flow: the agent proposes actions, a human approves/edits/rejects, and results feed back into the agent context.&lt;/em>&lt;/p>
&lt;p>This shared architecture means HITL is not a bolted-on feature — it&amp;rsquo;s a core part of how kagent agents communicate. Adding approval to a new tool is a one-line YAML change, not a code change.&lt;/p>
&lt;hr>
&lt;h2 id="building-a-hitl-agent-step-by-step">Building a HITL Agent: Step by Step&lt;/h2>
&lt;p>Let&amp;rsquo;s build one. We&amp;rsquo;ll create an agent with access to Kubernetes tools — read-only tools run freely, destructive tools require approval.&lt;/p>
&lt;h3 id="prerequisites">Prerequisites&lt;/h3>
&lt;ul>
&lt;li>Docker, Kind, kubectl, Helm installed&lt;/li>
&lt;li>An OpenAI API key&lt;/li>
&lt;/ul>
&lt;h3 id="1-create-a-cluster">1. Create a Cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name kagent-hitl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-install-kagent">2. Install kagent&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install kagent-crds oci://ghcr.io/kagent-dev/kagent/helm/kagent-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set your API key&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install kagent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install kagent oci://ghcr.io/kagent-dev/kagent/helm/kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kagent &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.default&lt;span class="o">=&lt;/span>openAI &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set providers.openAI.apiKey&lt;span class="o">=&lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for pods&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>ready pod --all -n kagent --timeout&lt;span class="o">=&lt;/span>120s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-define-the-agent">3. Define the Agent&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev/v1alpha2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">hitl-agent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">A Kubernetes agent with human-in-the-loop approval for destructive operations.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Declarative&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">declarative&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">modelConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">default-model-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">systemMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> You are a Kubernetes management agent. You help users inspect and manage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> resources in the cluster. Before making any changes, explain what you
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> plan to do. If the user&amp;#39;s request is ambiguous, use the ask_user tool
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> to clarify before proceeding.&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tools&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">McpServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcpServer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent-tool-server&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RemoteMCPServer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">apiGroup&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kagent.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">toolNames&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resources&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_describe_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_pod_logs&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_events&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_get_resource_yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_patch_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requireApproval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_apply_manifest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_delete_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">k8s_patch_resource&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create -f hitl-agent.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="4-open-the-ui">4. Open the UI&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n kagent svc/kagent-ui 8080:8080
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open &lt;a href="http://localhost:8080">http://localhost:8080&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="seeing-it-in-action">Seeing It in Action&lt;/h2>
&lt;h3 id="scenario-1-reading-is-free">Scenario 1: Reading is Free&lt;/h3>
&lt;p>You ask the agent: &lt;em>&amp;ldquo;List all pods in the kagent namespace.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent calls &lt;code>k8s_get_resources&lt;/code>. Since this tool is &lt;strong>not&lt;/strong> in &lt;code>requireApproval&lt;/code>, it runs immediately. You see the results with no interruption.&lt;/p>
&lt;p>This is the right UX. You don&amp;rsquo;t want to click &amp;ldquo;Approve&amp;rdquo; every time you ask the agent to read something. Only destructive operations should have gates.&lt;/p>
&lt;h3 id="scenario-2-writes-need-approval">Scenario 2: Writes Need Approval&lt;/h3>
&lt;p>You ask: &lt;em>&amp;ldquo;Create a ConfigMap called test-config in the default namespace with the key message set to &amp;lsquo;hello from kagent&amp;rsquo;.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent calls &lt;code>k8s_apply_manifest&lt;/code>. Since this tool &lt;strong>is&lt;/strong> in &lt;code>requireApproval&lt;/code>, execution pauses. In the UI, you see:&lt;/p>
&lt;ul>
&lt;li>The tool the agent wants to call&lt;/li>
&lt;li>The arguments it wants to pass (including the full YAML manifest)&lt;/li>
&lt;li>&lt;strong>Approve&lt;/strong> and &lt;strong>Reject&lt;/strong> buttons&lt;/li>
&lt;/ul>
&lt;p>You review the manifest. It looks correct. You click &lt;strong>Approve&lt;/strong>. The agent applies the ConfigMap and confirms success.&lt;/p>
&lt;h3 id="scenario-3-rejection-with-reason">Scenario 3: Rejection with Reason&lt;/h3>
&lt;p>You ask: &lt;em>&amp;ldquo;Delete the ConfigMap test-config in the default namespace.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent calls &lt;code>k8s_delete_resource&lt;/code>. Execution pauses. But this time, you click &lt;strong>Reject&lt;/strong> and enter a reason: &lt;em>&amp;ldquo;I want to keep this ConfigMap for now.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent receives your rejection reason. Instead of failing or retrying, it acknowledges your decision and responds: &lt;em>&amp;ldquo;Understood — I&amp;rsquo;ll leave the ConfigMap in place. Let me know if you change your mind.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>This is a key detail. The rejection reason isn&amp;rsquo;t just logged — it&amp;rsquo;s sent back to the LLM as context. The agent can adapt its behavior based on &lt;strong>why&lt;/strong> you said no.&lt;/p>
&lt;h3 id="scenario-4-agent-asks-for-clarification">Scenario 4: Agent Asks for Clarification&lt;/h3>
&lt;p>You ask: &lt;em>&amp;ldquo;Set up a namespace for my application.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>The agent doesn&amp;rsquo;t know what to call the namespace. Instead of guessing, it calls &lt;code>ask_user&lt;/code>: &lt;em>&amp;ldquo;What should the namespace be called?&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>You type &amp;ldquo;staging-app&amp;rdquo; and the agent continues, creating the namespace with the name you specified.&lt;/p>
&lt;p>This is the difference between an agent that guesses wrong and an agent that gets it right. The cost of asking is a few seconds. The cost of guessing wrong is rolling back a mistake.&lt;/p>
&lt;hr>
&lt;h2 id="design-principles">Design Principles&lt;/h2>
&lt;p>A few things about kagent&amp;rsquo;s HITL implementation that I think are worth highlighting:&lt;/p>
&lt;h3 id="1-opt-in-not-opt-out">1. Opt-In, Not Opt-Out&lt;/h3>
&lt;p>Tools run freely by default. You explicitly list which ones need approval. This means adding a new read-only tool to an agent doesn&amp;rsquo;t require updating any approval configuration — it just works.&lt;/p>
&lt;h3 id="2-declarative-not-procedural">2. Declarative, Not Procedural&lt;/h3>
&lt;p>You don&amp;rsquo;t write approval logic in code. You add a tool name to a YAML list. This keeps agent definitions readable and auditable. A security reviewer can look at the agent YAML and immediately see which tools require human approval.&lt;/p>
&lt;h3 id="3-rejection-is-contextual">3. Rejection Is Contextual&lt;/h3>
&lt;p>When you reject a tool call, you can provide a reason. That reason is returned to the LLM as part of the conversation context. This means the agent can learn from your rejection in the current conversation and adjust its approach.&lt;/p>
&lt;h3 id="4-ask_user-is-always-available">4. ask_user Is Always Available&lt;/h3>
&lt;p>You don&amp;rsquo;t need to configure &lt;code>ask_user&lt;/code>. It&amp;rsquo;s built-in on every agent. This encourages agent builders to write system prompts that tell the agent to ask when things are ambiguous, rather than assuming.&lt;/p>
&lt;hr>
&lt;h2 id="when-to-use-hitl-and-when-not-to">When to Use HITL (and When Not To)&lt;/h2>
&lt;p>&lt;strong>Use HITL for:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Any operation that changes cluster state (create, update, delete)&lt;/li>
&lt;li>Operations that are hard to reverse (deleting PVCs, removing finalizers)&lt;/li>
&lt;li>Multi-step workflows where early decisions affect later steps&lt;/li>
&lt;li>Any agent that will be used by people who aren&amp;rsquo;t the agent&amp;rsquo;s author&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Skip HITL for:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Read-only operations (listing, describing, reading logs)&lt;/li>
&lt;li>Internal agent reasoning (tool calls that don&amp;rsquo;t affect external state)&lt;/li>
&lt;li>Fully automated pipelines where latency matters and rollback is cheap&lt;/li>
&lt;/ul>
&lt;p>The goal isn&amp;rsquo;t to make every action require approval — it&amp;rsquo;s to put gates where they matter.&lt;/p>
&lt;hr>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name kagent-hitl
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;p>HITL is one piece of the puzzle. kagent also supports:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>A2A protocol&lt;/strong> for agent-to-agent communication&lt;/li>
&lt;li>&lt;strong>MCP servers&lt;/strong> for extending agent capabilities with custom tools&lt;/li>
&lt;li>&lt;strong>Memory&lt;/strong> for agents that learn from past interactions&lt;/li>
&lt;li>&lt;strong>Skills&lt;/strong> for packaging reusable agent behaviors&lt;/li>
&lt;/ul>
&lt;p>If you want to try it yourself, the upstream Episode 03 material is here:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/solo-io/allthings/tree/main/episode-03">https://github.com/solo-io/allthings/tree/main/episode-03&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>kagent is open source: &lt;a href="https://github.com/kagent-dev/kagent">https://github.com/kagent-dev/kagent&lt;/a>&lt;/p>
&lt;hr>
&lt;p>&lt;em>This post is part of the &amp;ldquo;All Things&amp;rdquo; series exploring AI agents on Kubernetes.&lt;/em>&lt;/p></content:encoded></item><item><title>Prompting After Feb 2026: Prompt Craft → Context → Intent → Specs</title><link>https://maniak.io/articles/2026-02-27-prompting-post-feb-2026/</link><pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-27-prompting-post-feb-2026/</guid><description>&lt;p>Most people mean “write better prompts” when they say &lt;em>prompt engineering&lt;/em>.&lt;/p>
&lt;p>That was a winning strategy in 2024–2025, when the dominant workflow was:&lt;/p>
&lt;blockquote>
&lt;p>ask in chat → get an answer → iterate in real time&lt;/p>
&lt;/blockquote>
&lt;p>But as models become longer-running and more autonomous, the bottleneck shifts. You don’t get to babysit the session. You have to &lt;strong>encode oversight up front&lt;/strong>.&lt;/p>
&lt;p>This post is a practical distillation of a YouTube video that argues “prompting” is now hiding &lt;strong>four different disciplines&lt;/strong> — and you need all four to get consistent, production-quality outcomes.&lt;/p>
&lt;p>Source video: &lt;a href="https://youtu.be/BpibZSMGtdY">https://youtu.be/BpibZSMGtdY&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="the-four-disciplines-of-prompting-in-2026">The Four Disciplines of “Prompting” in 2026&lt;/h2>
&lt;h3 id="1-prompt-craft-table-stakes">1) Prompt craft (table stakes)&lt;/h3>
&lt;p>This is the classic skill:&lt;/p>
&lt;ul>
&lt;li>Clear instruction&lt;/li>
&lt;li>Examples + counterexamples&lt;/li>
&lt;li>Guardrails (what to do / what not to do)&lt;/li>
&lt;li>Explicit output format&lt;/li>
&lt;li>Rules for ambiguity (“if X conflicts with Y, do Z”)&lt;/li>
&lt;/ul>
&lt;p>It still matters — it just doesn’t differentiate you anymore.&lt;/p>
&lt;h3 id="2-context-engineering-your-real-leverage">2) Context engineering (your real leverage)&lt;/h3>
&lt;p>Your prompt might be 200 tokens.
Your context window might be 200k–1M.&lt;/p>
&lt;p>That means your “prompt” is a rounding error.&lt;/p>
&lt;p>Context engineering is the work of designing the information environment the agent runs inside:&lt;/p>
&lt;ul>
&lt;li>system prompts / agent instructions&lt;/li>
&lt;li>tool definitions + permissions&lt;/li>
&lt;li>RAG sources / docs / repos&lt;/li>
&lt;li>memory (what persists across runs)&lt;/li>
&lt;li>conventions (how this org writes, builds, tests, ships)&lt;/li>
&lt;/ul>
&lt;p>If you see someone getting 10x more out of the same model, they usually aren’t “better at wording.”
They built better context infrastructure.&lt;/p>
&lt;h3 id="3-intent-engineering-what-the-agent-should-want">3) Intent engineering (what the agent should &lt;em>want&lt;/em>)&lt;/h3>
&lt;p>Context tells the agent &lt;strong>what to know&lt;/strong>.
Intent tells the agent &lt;strong>what to optimize for&lt;/strong>.&lt;/p>
&lt;p>This is where teams break things at enterprise scale:&lt;/p>
&lt;ul>
&lt;li>speed vs quality&lt;/li>
&lt;li>cost vs correctness&lt;/li>
&lt;li>customer satisfaction vs ticket closure time&lt;/li>
&lt;li>“ship it” vs “fail safe”&lt;/li>
&lt;/ul>
&lt;p>If you don’t encode trade-offs and escalation triggers, the agent will “pick a metric” implicitly.&lt;/p>
&lt;h3 id="4-specification-engineering-blueprints-for-autonomous-work">4) Specification engineering (blueprints for autonomous work)&lt;/h3>
&lt;p>Specs are what you write when you can’t rely on real-time correction.&lt;/p>
&lt;p>A good spec is:&lt;/p>
&lt;ul>
&lt;li>self-contained&lt;/li>
&lt;li>structured&lt;/li>
&lt;li>internally consistent&lt;/li>
&lt;li>explicit about quality measurement&lt;/li>
&lt;/ul>
&lt;p>If your output keeps coming back “80% correct,” you don’t have a prompt problem.
You have a spec + evaluation problem.&lt;/p>
&lt;h4 id="what-specification-engineering-actually-means">What “specification engineering” actually means&lt;/h4>
&lt;p>The video’s argument is simple: once agents can run for hours or days, you can’t rely on live back-and-forth to fix mistakes.&lt;/p>
&lt;p>So specification engineering becomes the skill of writing &lt;strong>agent-executable blueprints&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Self-contained&lt;/strong>: no hidden assumptions, undefined acronyms, or missing “obvious” org context.&lt;/li>
&lt;li>&lt;strong>Verifiable&lt;/strong>: “done” is defined in checks that someone else can evaluate.&lt;/li>
&lt;li>&lt;strong>Constrained&lt;/strong>: must/must-not/preferences are explicit (plus what should be escalated).&lt;/li>
&lt;li>&lt;strong>Decomposable&lt;/strong>: the work can be broken into chunks that can be executed and verified independently.&lt;/li>
&lt;/ul>
&lt;p>The broader implication: your &lt;em>documents&lt;/em> become infrastructure. Strategy docs, product docs, runbooks, and OKRs all start acting like specs that agents (and humans) can execute.&lt;/p>
&lt;h4 id="the-five-spec-primitives-a-useful-mental-model">The five spec primitives (a useful mental model)&lt;/h4>
&lt;ol>
&lt;li>&lt;strong>Self-contained problem statement&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Acceptance criteria&lt;/strong> (verifiable “done”)&lt;/li>
&lt;li>&lt;strong>Constraint architecture&lt;/strong> (must / must-not / preferences / escalate if)&lt;/li>
&lt;li>&lt;strong>Decomposition&lt;/strong> (small, independently testable chunks)&lt;/li>
&lt;li>&lt;strong>Eval design&lt;/strong> (test cases that catch regressions after model updates)&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="the-2026-prompting-checklist-copypaste">The 2026 Prompting Checklist (copy/paste)&lt;/h2>
&lt;p>Use this before you hand a long-running task to an agent.&lt;/p>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Objective&lt;/strong>: what outcome do you want, and why?&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Success metric&lt;/strong>: how will we measure success?&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Inputs&lt;/strong>: links/docs/data sources (authoritative only)&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Definitions&lt;/strong>: terms/acronyms the agent might misread&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Deliverables&lt;/strong>: files/sections/artifacts + exact output format&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Acceptance criteria&lt;/strong>: verifiable checks (someone else can evaluate)&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Constraints&lt;/strong>:
&lt;ul>
&lt;li>Must&lt;/li>
&lt;li>Must not&lt;/li>
&lt;li>Preferences&lt;/li>
&lt;li>Escalate if&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Plan-first&lt;/strong>: ask for a plan + checkpoints before execution&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Decomposition&lt;/strong>: tasks broken into verifiable sub-steps&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Eval&lt;/strong>: test cases / verification steps&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Progress log&lt;/strong>: so the next session doesn’t start blind&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="templates-use-these-as-your-default-prompt-format">Templates (use these as your default “prompt format”)&lt;/h2>
&lt;h3 id="template-a--self-contained-spec-general">Template A — Self-contained spec (general)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Objective
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> What to do:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Why it matters:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Audience:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Inputs (authoritative)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Links/docs:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Definitions/glossary:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Deliverables
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> D1:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> D2:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Acceptance criteria
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> [ ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> [ ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Must:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Must not:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Preferences:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Escalate if:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Workflow
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>1) Propose plan + checkpoints.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2) Wait for confirmation.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3) Execute.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">4) Provide final output + verification notes.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="template-b--coding-task-spec">Template B — Coding task spec&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Repo context
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Repo root:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Build commands:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Test commands:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Conventions (style, lint, naming, etc.):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Change request
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Feature/bug:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Non-goals:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Must not break:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Security/perf constraints:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Escalate if:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Acceptance criteria
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Tests added/updated:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Commands that must pass:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Manual validation steps:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Rollout
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Backwards compatibility:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Migration steps:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="template-c--research-task-spec">Template C — Research task spec&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Question
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Time horizon:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Must cite primary sources:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Output format:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Deliverables
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Executive summary (5 bullets)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Findings (with links)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Risks/unknowns
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Recommendations
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Evaluation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> What would make this wrong?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> How can we verify quickly?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="failure-modes-i-see-constantly-and-how-to-fix-them">Failure Modes I See Constantly (and how to fix them)&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>“It’s 80% right but takes forever to clean up.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: acceptance criteria + explicit output format + examples/counterexamples.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“The agent drifted after 30 minutes.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: constraints + escalation triggers + checkpoints + progress log.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“We loaded everything and quality got worse.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: curate context; summarize; move stable conventions into a short, high-signal rule file.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“It optimized for the wrong thing.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: intent engineering — explicitly state trade-offs and priorities.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“We can’t tell if outputs are good.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: eval design — build test cases; rerun after model updates; track regressions.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="what-to-do-this-week">What to Do This Week&lt;/h2>
&lt;p>If you want an easy, practical on-ramp:&lt;/p>
&lt;ol>
&lt;li>Pick &lt;strong>one recurring task&lt;/strong> (deck creation, incident writeups, migration plans, content outlines).&lt;/li>
&lt;li>Write a &lt;strong>self-contained spec&lt;/strong> using Template A.&lt;/li>
&lt;li>Define &lt;strong>acceptance criteria&lt;/strong> that another person could verify.&lt;/li>
&lt;li>Create &lt;strong>3–5 eval cases&lt;/strong> (known-good examples).&lt;/li>
&lt;li>Save it as your baseline prompt/spec and iterate as models change.&lt;/li>
&lt;/ol>
&lt;p>That’s the shift: from “chat tricks” to &lt;strong>repeatable systems&lt;/strong>.&lt;/p></description><content:encoded>&lt;p>Most people mean “write better prompts” when they say &lt;em>prompt engineering&lt;/em>.&lt;/p>
&lt;p>That was a winning strategy in 2024–2025, when the dominant workflow was:&lt;/p>
&lt;blockquote>
&lt;p>ask in chat → get an answer → iterate in real time&lt;/p>
&lt;/blockquote>
&lt;p>But as models become longer-running and more autonomous, the bottleneck shifts. You don’t get to babysit the session. You have to &lt;strong>encode oversight up front&lt;/strong>.&lt;/p>
&lt;p>This post is a practical distillation of a YouTube video that argues “prompting” is now hiding &lt;strong>four different disciplines&lt;/strong> — and you need all four to get consistent, production-quality outcomes.&lt;/p>
&lt;p>Source video: &lt;a href="https://youtu.be/BpibZSMGtdY">https://youtu.be/BpibZSMGtdY&lt;/a>&lt;/p>
&lt;hr>
&lt;h2 id="the-four-disciplines-of-prompting-in-2026">The Four Disciplines of “Prompting” in 2026&lt;/h2>
&lt;h3 id="1-prompt-craft-table-stakes">1) Prompt craft (table stakes)&lt;/h3>
&lt;p>This is the classic skill:&lt;/p>
&lt;ul>
&lt;li>Clear instruction&lt;/li>
&lt;li>Examples + counterexamples&lt;/li>
&lt;li>Guardrails (what to do / what not to do)&lt;/li>
&lt;li>Explicit output format&lt;/li>
&lt;li>Rules for ambiguity (“if X conflicts with Y, do Z”)&lt;/li>
&lt;/ul>
&lt;p>It still matters — it just doesn’t differentiate you anymore.&lt;/p>
&lt;h3 id="2-context-engineering-your-real-leverage">2) Context engineering (your real leverage)&lt;/h3>
&lt;p>Your prompt might be 200 tokens.
Your context window might be 200k–1M.&lt;/p>
&lt;p>That means your “prompt” is a rounding error.&lt;/p>
&lt;p>Context engineering is the work of designing the information environment the agent runs inside:&lt;/p>
&lt;ul>
&lt;li>system prompts / agent instructions&lt;/li>
&lt;li>tool definitions + permissions&lt;/li>
&lt;li>RAG sources / docs / repos&lt;/li>
&lt;li>memory (what persists across runs)&lt;/li>
&lt;li>conventions (how this org writes, builds, tests, ships)&lt;/li>
&lt;/ul>
&lt;p>If you see someone getting 10x more out of the same model, they usually aren’t “better at wording.”
They built better context infrastructure.&lt;/p>
&lt;h3 id="3-intent-engineering-what-the-agent-should-want">3) Intent engineering (what the agent should &lt;em>want&lt;/em>)&lt;/h3>
&lt;p>Context tells the agent &lt;strong>what to know&lt;/strong>.
Intent tells the agent &lt;strong>what to optimize for&lt;/strong>.&lt;/p>
&lt;p>This is where teams break things at enterprise scale:&lt;/p>
&lt;ul>
&lt;li>speed vs quality&lt;/li>
&lt;li>cost vs correctness&lt;/li>
&lt;li>customer satisfaction vs ticket closure time&lt;/li>
&lt;li>“ship it” vs “fail safe”&lt;/li>
&lt;/ul>
&lt;p>If you don’t encode trade-offs and escalation triggers, the agent will “pick a metric” implicitly.&lt;/p>
&lt;h3 id="4-specification-engineering-blueprints-for-autonomous-work">4) Specification engineering (blueprints for autonomous work)&lt;/h3>
&lt;p>Specs are what you write when you can’t rely on real-time correction.&lt;/p>
&lt;p>A good spec is:&lt;/p>
&lt;ul>
&lt;li>self-contained&lt;/li>
&lt;li>structured&lt;/li>
&lt;li>internally consistent&lt;/li>
&lt;li>explicit about quality measurement&lt;/li>
&lt;/ul>
&lt;p>If your output keeps coming back “80% correct,” you don’t have a prompt problem.
You have a spec + evaluation problem.&lt;/p>
&lt;h4 id="what-specification-engineering-actually-means">What “specification engineering” actually means&lt;/h4>
&lt;p>The video’s argument is simple: once agents can run for hours or days, you can’t rely on live back-and-forth to fix mistakes.&lt;/p>
&lt;p>So specification engineering becomes the skill of writing &lt;strong>agent-executable blueprints&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Self-contained&lt;/strong>: no hidden assumptions, undefined acronyms, or missing “obvious” org context.&lt;/li>
&lt;li>&lt;strong>Verifiable&lt;/strong>: “done” is defined in checks that someone else can evaluate.&lt;/li>
&lt;li>&lt;strong>Constrained&lt;/strong>: must/must-not/preferences are explicit (plus what should be escalated).&lt;/li>
&lt;li>&lt;strong>Decomposable&lt;/strong>: the work can be broken into chunks that can be executed and verified independently.&lt;/li>
&lt;/ul>
&lt;p>The broader implication: your &lt;em>documents&lt;/em> become infrastructure. Strategy docs, product docs, runbooks, and OKRs all start acting like specs that agents (and humans) can execute.&lt;/p>
&lt;h4 id="the-five-spec-primitives-a-useful-mental-model">The five spec primitives (a useful mental model)&lt;/h4>
&lt;ol>
&lt;li>&lt;strong>Self-contained problem statement&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Acceptance criteria&lt;/strong> (verifiable “done”)&lt;/li>
&lt;li>&lt;strong>Constraint architecture&lt;/strong> (must / must-not / preferences / escalate if)&lt;/li>
&lt;li>&lt;strong>Decomposition&lt;/strong> (small, independently testable chunks)&lt;/li>
&lt;li>&lt;strong>Eval design&lt;/strong> (test cases that catch regressions after model updates)&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="the-2026-prompting-checklist-copypaste">The 2026 Prompting Checklist (copy/paste)&lt;/h2>
&lt;p>Use this before you hand a long-running task to an agent.&lt;/p>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Objective&lt;/strong>: what outcome do you want, and why?&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Success metric&lt;/strong>: how will we measure success?&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Inputs&lt;/strong>: links/docs/data sources (authoritative only)&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Definitions&lt;/strong>: terms/acronyms the agent might misread&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Deliverables&lt;/strong>: files/sections/artifacts + exact output format&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Acceptance criteria&lt;/strong>: verifiable checks (someone else can evaluate)&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Constraints&lt;/strong>:
&lt;ul>
&lt;li>Must&lt;/li>
&lt;li>Must not&lt;/li>
&lt;li>Preferences&lt;/li>
&lt;li>Escalate if&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Plan-first&lt;/strong>: ask for a plan + checkpoints before execution&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Decomposition&lt;/strong>: tasks broken into verifiable sub-steps&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Eval&lt;/strong>: test cases / verification steps&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> &lt;strong>Progress log&lt;/strong>: so the next session doesn’t start blind&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="templates-use-these-as-your-default-prompt-format">Templates (use these as your default “prompt format”)&lt;/h2>
&lt;h3 id="template-a--self-contained-spec-general">Template A — Self-contained spec (general)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Objective
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> What to do:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Why it matters:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Audience:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Inputs (authoritative)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Links/docs:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Definitions/glossary:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Deliverables
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> D1:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> D2:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Acceptance criteria
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> [ ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> [ ]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Must:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Must not:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Preferences:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Escalate if:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Workflow
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>1) Propose plan + checkpoints.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2) Wait for confirmation.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">3) Execute.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">4) Provide final output + verification notes.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="template-b--coding-task-spec">Template B — Coding task spec&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Repo context
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Repo root:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Build commands:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Test commands:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Conventions (style, lint, naming, etc.):
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Change request
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Feature/bug:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Non-goals:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Must not break:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Security/perf constraints:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Escalate if:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Acceptance criteria
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Tests added/updated:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Commands that must pass:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Manual validation steps:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Rollout
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Backwards compatibility:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Migration steps:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="template-c--research-task-spec">Template C — Research task spec&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Question
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Constraints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Time horizon:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Must cite primary sources:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Output format:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Deliverables
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> Executive summary (5 bullets)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Findings (with links)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Risks/unknowns
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> Recommendations
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Evaluation
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>&lt;span class="k">-&lt;/span> What would make this wrong?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> How can we verify quickly?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="failure-modes-i-see-constantly-and-how-to-fix-them">Failure Modes I See Constantly (and how to fix them)&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>“It’s 80% right but takes forever to clean up.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: acceptance criteria + explicit output format + examples/counterexamples.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“The agent drifted after 30 minutes.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: constraints + escalation triggers + checkpoints + progress log.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“We loaded everything and quality got worse.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: curate context; summarize; move stable conventions into a short, high-signal rule file.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“It optimized for the wrong thing.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: intent engineering — explicitly state trade-offs and priorities.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>“We can’t tell if outputs are good.”&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Fix: eval design — build test cases; rerun after model updates; track regressions.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="what-to-do-this-week">What to Do This Week&lt;/h2>
&lt;p>If you want an easy, practical on-ramp:&lt;/p>
&lt;ol>
&lt;li>Pick &lt;strong>one recurring task&lt;/strong> (deck creation, incident writeups, migration plans, content outlines).&lt;/li>
&lt;li>Write a &lt;strong>self-contained spec&lt;/strong> using Template A.&lt;/li>
&lt;li>Define &lt;strong>acceptance criteria&lt;/strong> that another person could verify.&lt;/li>
&lt;li>Create &lt;strong>3–5 eval cases&lt;/strong> (known-good examples).&lt;/li>
&lt;li>Save it as your baseline prompt/spec and iterate as models change.&lt;/li>
&lt;/ol>
&lt;p>That’s the shift: from “chat tricks” to &lt;strong>repeatable systems&lt;/strong>.&lt;/p></content:encoded></item><item><title>Configuring Local Qwen3 Embeddings for OpenClaw Memory</title><link>https://maniak.io/articles/2026-02-23-configuring-local-qwen3-embeddings-for-openclaw-memory/</link><pubDate>Mon, 23 Feb 2026 15:19:00 -0500</pubDate><guid>https://maniak.io/articles/2026-02-23-configuring-local-qwen3-embeddings-for-openclaw-memory/</guid><description>&lt;h1 id="configuring-local-qwen3-embeddings-for-openclaws-memory-system">Configuring Local Qwen3 Embeddings for OpenClaw&amp;rsquo;s Memory System&lt;/h1>
&lt;p>OpenClaw&amp;rsquo;s &lt;code>memory_search&lt;/code> and &lt;code>memory_get&lt;/code> tools enable semantic recall from &lt;code>MEMORY.md&lt;/code>, &lt;code>memory/*.md&lt;/code>, and session transcripts &lt;strong>before&lt;/strong> answering about prior work/decisions. Defaults use cloud embeddings, but I&amp;rsquo;ve switched to a &lt;strong>local Qwen3-Embedding-0.6B&lt;/strong> (GGUF quantized) for privacy, speed, and zero cost.&lt;/p>
&lt;h2 id="why-qwen3-embedding-06b">Why Qwen3-Embedding-0.6B?&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Sweet Spot&lt;/strong>: 0.6B params—20-100ms/chunk on CPU (10x faster than 7B).&lt;/li>
&lt;li>&lt;strong>Quality&lt;/strong>: Tops MTEB benchmarks for retrieval, beats E5-small.&lt;/li>
&lt;li>&lt;strong>GGUF&lt;/strong>: Q8_0 (~400MB, near-lossless) via llama.cpp.&lt;/li>
&lt;li>&lt;strong>Hybrid Mode&lt;/strong>: CPU + auto-GPU (CUDA/Metal/ROCm).&lt;/li>
&lt;/ul>
&lt;h2 id="model-specs">Model Specs&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Repo&lt;/strong>: &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF">Qwen/Qwen3-Embedding-0.6B-GGUF&lt;/a>&lt;/li>
&lt;li>&lt;strong>File&lt;/strong>: &lt;code>Qwen3-Embedding-0.6B-Q8_0.gguf&lt;/code>&lt;/li>
&lt;li>&lt;strong>Dims&lt;/strong>: 1024 (semantic search optimized)&lt;/li>
&lt;li>&lt;strong>Provider&lt;/strong>: &lt;code>local&lt;/code> (llama.cpp backend)&lt;/li>
&lt;/ul>
&lt;h2 id="setup-5-mins">Setup (5 Mins)&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Download&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mkdir&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">~/.&lt;/span>&lt;span class="n">openclaw&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cd&lt;/span> &lt;span class="o">~/.&lt;/span>&lt;span class="n">openclaw&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">huggingface&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cli&lt;/span> &lt;span class="n">download&lt;/span> &lt;span class="n">Qwen&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Qwen3&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Embedding&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">0.6&lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">GGUF&lt;/span> &lt;span class="n">Qwen3&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Embedding&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">0.6&lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Q8_0&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">gguf&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Config&lt;/strong> (&lt;code>~/.openclaw/config.yaml&lt;/code> or CLI):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">agents&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaults&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">embeddingModel&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf?provider=local&amp;amp;hybrid=true&amp;amp;cpu_threads=8&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Params:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Key&lt;/th>
&lt;th>Desc&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>provider=local&lt;/code>&lt;/td>
&lt;td>llama.cpp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>hybrid=true&lt;/code>&lt;/td>
&lt;td>GPU auto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>cpu_threads=8&lt;/code>&lt;/td>
&lt;td>Cores&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>CLI: &lt;code>openclaw configure --section agents.defaults.embeddingModel '...'&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Restart&lt;/strong>: &lt;code>openclaw gateway restart&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Test&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">memory_search query=&amp;#34;test prior decision&amp;#34; # ~80ms top-5
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h2 id="benchmarks-i7-12700k-32gb-no-gpu">Benchmarks (i7-12700K, 32GB, No GPU)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Task&lt;/th>
&lt;th>Time&lt;/th>
&lt;th>RAM&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Embed 512t chunk&lt;/td>
&lt;td>45ms&lt;/td>
&lt;td>350MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search 100 docs&lt;/td>
&lt;td>80ms&lt;/td>
&lt;td>&amp;lt;500MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>100 queries/min&lt;/td>
&lt;td>✅&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>GPU (RTX 3060): 15ms/embed.&lt;/p>
&lt;h2 id="how-it-works">How It Works&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Chunk&lt;/strong>: Files → 512t snippets (w/ lines).&lt;/li>
&lt;li>&lt;strong>Embed Query&lt;/strong>: Qwen3 → vector.&lt;/li>
&lt;li>&lt;strong>SimSearch&lt;/strong>: Cosine top-k (minScore filter).&lt;/li>
&lt;li>&lt;strong>Cite&lt;/strong>: &lt;code>path#line&lt;/code> for &lt;code>memory_get&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>On-demand (snippet cache).&lt;/p>
&lt;h2 id="tweaks--troubleshooting">Tweaks &amp;amp; Troubleshooting&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Faster&lt;/strong>: Q4_K_M.gguf.&lt;/li>
&lt;li>&lt;strong>GPU?&lt;/strong>: &lt;code>gpu=true&lt;/code>.&lt;/li>
&lt;li>&lt;strong>OOM&lt;/strong>: &lt;code>context_length=2048&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Logs&lt;/strong>: &lt;code>journalctl -u openclaw-gateway | grep embedding&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Verify&lt;/strong>: &lt;code>session_status&lt;/code> → embedding provider.&lt;/li>
&lt;/ul>
&lt;h2 id="alternatives">Alternatives&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th>Params&lt;/th>
&lt;th>Speed&lt;/th>
&lt;th>Use Case&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Qwen3-0.6B-Q8&lt;/td>
&lt;td>0.6B&lt;/td>
&lt;td>45ms&lt;/td>
&lt;td>General&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NV-Embed-4B-Q5&lt;/td>
&lt;td>4B&lt;/td>
&lt;td>150ms&lt;/td>
&lt;td>Code&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>all-minilm-L6&lt;/td>
&lt;td>22M&lt;/td>
&lt;td>10ms&lt;/td>
&lt;td>Light&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="pro-tips">Pro Tips&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Pre-cache&lt;/strong>: JSONL embeddings via cron.&lt;/li>
&lt;li>&lt;strong>Scale&lt;/strong>: memory/ subdirs.&lt;/li>
&lt;li>&lt;strong>Monitor&lt;/strong>: Query hit rates.&lt;/li>
&lt;/ul>
&lt;p>Local RAG + Claude = unbeatable. Questions?&lt;/p>
&lt;p>&lt;a href="https://docs.openclaw.ai/tools/memory">Docs&lt;/a> | &lt;a href="https://huggingface.co/Qwen">Qwen&lt;/a>&lt;/p></description><content:encoded>&lt;h1 id="configuring-local-qwen3-embeddings-for-openclaws-memory-system">Configuring Local Qwen3 Embeddings for OpenClaw&amp;rsquo;s Memory System&lt;/h1>
&lt;p>OpenClaw&amp;rsquo;s &lt;code>memory_search&lt;/code> and &lt;code>memory_get&lt;/code> tools enable semantic recall from &lt;code>MEMORY.md&lt;/code>, &lt;code>memory/*.md&lt;/code>, and session transcripts &lt;strong>before&lt;/strong> answering about prior work/decisions. Defaults use cloud embeddings, but I&amp;rsquo;ve switched to a &lt;strong>local Qwen3-Embedding-0.6B&lt;/strong> (GGUF quantized) for privacy, speed, and zero cost.&lt;/p>
&lt;h2 id="why-qwen3-embedding-06b">Why Qwen3-Embedding-0.6B?&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Sweet Spot&lt;/strong>: 0.6B params—20-100ms/chunk on CPU (10x faster than 7B).&lt;/li>
&lt;li>&lt;strong>Quality&lt;/strong>: Tops MTEB benchmarks for retrieval, beats E5-small.&lt;/li>
&lt;li>&lt;strong>GGUF&lt;/strong>: Q8_0 (~400MB, near-lossless) via llama.cpp.&lt;/li>
&lt;li>&lt;strong>Hybrid Mode&lt;/strong>: CPU + auto-GPU (CUDA/Metal/ROCm).&lt;/li>
&lt;/ul>
&lt;h2 id="model-specs">Model Specs&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Repo&lt;/strong>: &lt;a href="https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF">Qwen/Qwen3-Embedding-0.6B-GGUF&lt;/a>&lt;/li>
&lt;li>&lt;strong>File&lt;/strong>: &lt;code>Qwen3-Embedding-0.6B-Q8_0.gguf&lt;/code>&lt;/li>
&lt;li>&lt;strong>Dims&lt;/strong>: 1024 (semantic search optimized)&lt;/li>
&lt;li>&lt;strong>Provider&lt;/strong>: &lt;code>local&lt;/code> (llama.cpp backend)&lt;/li>
&lt;/ul>
&lt;h2 id="setup-5-mins">Setup (5 Mins)&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Download&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">mkdir&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">p&lt;/span> &lt;span class="o">~/.&lt;/span>&lt;span class="n">openclaw&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">cd&lt;/span> &lt;span class="o">~/.&lt;/span>&lt;span class="n">openclaw&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">models&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">huggingface&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cli&lt;/span> &lt;span class="n">download&lt;/span> &lt;span class="n">Qwen&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Qwen3&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Embedding&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">0.6&lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">GGUF&lt;/span> &lt;span class="n">Qwen3&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Embedding&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mf">0.6&lt;/span>&lt;span class="n">B&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">Q8_0&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">gguf&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Config&lt;/strong> (&lt;code>~/.openclaw/config.yaml&lt;/code> or CLI):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">agents&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">defaults&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">embeddingModel&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf?provider=local&amp;amp;hybrid=true&amp;amp;cpu_threads=8&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Params:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Key&lt;/th>
&lt;th>Desc&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>provider=local&lt;/code>&lt;/td>
&lt;td>llama.cpp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>hybrid=true&lt;/code>&lt;/td>
&lt;td>GPU auto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>cpu_threads=8&lt;/code>&lt;/td>
&lt;td>Cores&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>CLI: &lt;code>openclaw configure --section agents.defaults.embeddingModel '...'&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Restart&lt;/strong>: &lt;code>openclaw gateway restart&lt;/code>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Test&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">memory_search query=&amp;#34;test prior decision&amp;#34; # ~80ms top-5
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ol>
&lt;h2 id="benchmarks-i7-12700k-32gb-no-gpu">Benchmarks (i7-12700K, 32GB, No GPU)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Task&lt;/th>
&lt;th>Time&lt;/th>
&lt;th>RAM&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Embed 512t chunk&lt;/td>
&lt;td>45ms&lt;/td>
&lt;td>350MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Search 100 docs&lt;/td>
&lt;td>80ms&lt;/td>
&lt;td>&amp;lt;500MB&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>100 queries/min&lt;/td>
&lt;td>✅&lt;/td>
&lt;td>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>GPU (RTX 3060): 15ms/embed.&lt;/p>
&lt;h2 id="how-it-works">How It Works&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Chunk&lt;/strong>: Files → 512t snippets (w/ lines).&lt;/li>
&lt;li>&lt;strong>Embed Query&lt;/strong>: Qwen3 → vector.&lt;/li>
&lt;li>&lt;strong>SimSearch&lt;/strong>: Cosine top-k (minScore filter).&lt;/li>
&lt;li>&lt;strong>Cite&lt;/strong>: &lt;code>path#line&lt;/code> for &lt;code>memory_get&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>On-demand (snippet cache).&lt;/p>
&lt;h2 id="tweaks--troubleshooting">Tweaks &amp;amp; Troubleshooting&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Faster&lt;/strong>: Q4_K_M.gguf.&lt;/li>
&lt;li>&lt;strong>GPU?&lt;/strong>: &lt;code>gpu=true&lt;/code>.&lt;/li>
&lt;li>&lt;strong>OOM&lt;/strong>: &lt;code>context_length=2048&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Logs&lt;/strong>: &lt;code>journalctl -u openclaw-gateway | grep embedding&lt;/code>.&lt;/li>
&lt;li>&lt;strong>Verify&lt;/strong>: &lt;code>session_status&lt;/code> → embedding provider.&lt;/li>
&lt;/ul>
&lt;h2 id="alternatives">Alternatives&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th>Params&lt;/th>
&lt;th>Speed&lt;/th>
&lt;th>Use Case&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Qwen3-0.6B-Q8&lt;/td>
&lt;td>0.6B&lt;/td>
&lt;td>45ms&lt;/td>
&lt;td>General&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NV-Embed-4B-Q5&lt;/td>
&lt;td>4B&lt;/td>
&lt;td>150ms&lt;/td>
&lt;td>Code&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>all-minilm-L6&lt;/td>
&lt;td>22M&lt;/td>
&lt;td>10ms&lt;/td>
&lt;td>Light&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="pro-tips">Pro Tips&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Pre-cache&lt;/strong>: JSONL embeddings via cron.&lt;/li>
&lt;li>&lt;strong>Scale&lt;/strong>: memory/ subdirs.&lt;/li>
&lt;li>&lt;strong>Monitor&lt;/strong>: Query hit rates.&lt;/li>
&lt;/ul>
&lt;p>Local RAG + Claude = unbeatable. Questions?&lt;/p>
&lt;p>&lt;a href="https://docs.openclaw.ai/tools/memory">Docs&lt;/a> | &lt;a href="https://huggingface.co/Qwen">Qwen&lt;/a>&lt;/p></content:encoded></item><item><title>Multi-Agent Architecture with a Kill Switch: Why Every AI Agent Needs a Gateway</title><link>https://maniak.io/articles/2026-02-21-multi-agent-architecture-agentgateway-kill-switch/</link><pubDate>Sat, 21 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-21-multi-agent-architecture-agentgateway-kill-switch/</guid><description>&lt;h2 id="the-setup">The Setup&lt;/h2>
&lt;p>I run a multi-agent system. One coordinator agent handles user interaction, memory, and routing. Specialist sub-agents get spawned on demand for domain-specific tasks — security audits, network diagnostics, cloud management, infrastructure automation. Each specialist has its own system prompt, its own toolset, and runs on a different model.&lt;/p>
&lt;p>It works. The specialists are good at their jobs. The coordinator knows when to delegate and when to handle things itself.&lt;/p>
&lt;p>But here&amp;rsquo;s what keeps me up at night: &lt;strong>what happens when one of these agents goes rogue?&lt;/strong>&lt;/p>
&lt;p>A security agent with access to nmap and trivy decides to scan every host on the network in a loop. A cloud agent burns through $500 of Opus tokens chasing a hallucinated Terraform state or decides to reconfigure your Istio ambient mesh routing because it misread a waypoint proxy status. A general agent with SSH access starts &amp;ldquo;fixing&amp;rdquo; things on production hosts that don&amp;rsquo;t need fixing.&lt;/p>
&lt;p>Without a control plane between your agents and the outside world, you have no way to stop any of this. No kill switch. No cost ceiling. No audit trail. No rate limits. Just agents with direct access to LLMs and tools, hoping nothing goes wrong.&lt;/p>
&lt;p>That&amp;rsquo;s not engineering. That&amp;rsquo;s negligence.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture">The Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s what I actually run. Every LLM call and every MCP tool invocation from every agent — coordinator and specialists alike — routes through &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ User (Seb) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Telegram / Discord / CLI │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────────────────────┬────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Coordinator Agent │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (Jacob) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • User interaction &amp;amp; conversation │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Memory management (MEMORY.md) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Task triage &amp;amp; routing │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Context assembly for specialists │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Result synthesis &amp;amp; delivery │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──┬──────────┬──────────┬──────────┬─────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼ ▼ ▼ ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Sec │ │ Net │ │Cloud │ │ General │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│Agent │ │Agent │ │Agent │ │ Agent │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──┬───┘ └──┬───┘ └──┬───┘ └────┬─────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┴─────────┴────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ALL traffic
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Kubernetes Cluster │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌──────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ agentgateway │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ (Pod) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Kill switch │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Rate limiting │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Cost controls │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • JWT auth + RBAC │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Observability (OTel) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Tool poisoning protection │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └──────┬───────────┬───────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┼───────────┼────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────▼────┐ ┌───▼────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ LLMs │ │ MCP Servers │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Anthropic│ │ nmap, trivy │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenAI │ │ aws-cli, kubectl │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ xAI │ │ istioctl, docker │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┘ └───────────────────-┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nothing reaches an LLM or a tool without passing through the gateway. That&amp;rsquo;s the entire point.&lt;/p>
&lt;hr>
&lt;h2 id="the-agents">The Agents&lt;/h2>
&lt;h3 id="coordinator-jacob">Coordinator: Jacob&lt;/h3>
&lt;p>The coordinator is the only agent that talks to the user. It owns the conversation, manages memory (a persistent &lt;code>MEMORY.md&lt;/code> that carries context across sessions), and decides which specialist to invoke for each request.&lt;/p>
&lt;p>When a task comes in, the coordinator classifies it and builds a context payload — the relevant portion of memory, the specific question, any constraints — and spawns a specialist. The specialist does its work, returns a result, and dies. Stateless. Disposable.&lt;/p>
&lt;p>The coordinator synthesizes the result and delivers it back to the user. If a task spans multiple domains, the coordinator fans out to multiple specialists in parallel.&lt;/p>
&lt;p>&lt;strong>Model&lt;/strong>: Sonnet — fast enough for routing, smart enough for context assembly.&lt;/p>
&lt;h3 id="security-agent">Security Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: Vulnerability scanning, CVE analysis, firewall rules, IAM audits, compliance checks&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: nmap, trivy, falco, OWASP ZAP, CIS benchmarks, secrets scanning&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Opus — high reasoning for threat analysis&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Read-only on infra by default, escalation required for remediation&lt;/li>
&lt;/ul>
&lt;h3 id="network-agent">Network Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: DNS, routing, load balancing, VPN, firewall config, traffic analysis&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: dig, traceroute, tcpdump, iperf3, netstat, ip, iptables, tshark&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Sonnet — fast, good for diagnostic tasks&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Network interfaces, DNS servers, routing tables&lt;/li>
&lt;/ul>
&lt;h3 id="cloud-agent">Cloud Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: AWS/GCP/Azure resource management, Terraform, cost optimization, architecture, Kubernetes, Istio service mesh, ambient mesh&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: aws-cli, gcloud, az, terraform, kubectl, helm, istioctl&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Sonnet — balance of speed and capability&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Cloud provider credentials (scoped IAM roles), Kubernetes clusters, Istio control plane&lt;/li>
&lt;/ul>
&lt;h3 id="general--infra-agent">General / Infra Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: Proxmox, Docker, Linux admin, Git, CI/CD, general automation&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: ssh, docker, git, systemctl, proxmox API, cron&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Sonnet (routine ops) or Haiku (simple tasks)&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Full local system, Proxmox API, SSH to hosts&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="routing-logic">Routing Logic&lt;/h2>
&lt;p>The coordinator classifies each request and routes to the appropriate specialist:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Keywords&lt;/th>
&lt;th>Routes To&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CVE, vulnerability, audit, compliance, secrets&lt;/td>
&lt;td>Security Agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DNS, firewall, routing, VPN, latency, ports&lt;/td>
&lt;td>Network Agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS, Terraform, GCP, Azure, S3, EC2, cost, Istio, mesh, Kubernetes, k8s&lt;/td>
&lt;td>Cloud Agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>VM, Docker, git, systemd, Proxmox, backup&lt;/td>
&lt;td>General Agent&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Ambiguous requests stay with the coordinator. Multi-domain tasks fan out to multiple specialists in parallel.&lt;/p>
&lt;hr>
&lt;h2 id="why-every-agent-goes-through-the-gateway">Why Every Agent Goes Through the Gateway&lt;/h2>
&lt;p>This is the part that matters. Here&amp;rsquo;s why I don&amp;rsquo;t let any agent — not even the coordinator — talk to LLMs or tools directly.&lt;/p>
&lt;h3 id="the-doom-scenario">The Doom Scenario&lt;/h3>
&lt;p>Picture this: your cloud agent is debugging a Terraform plan. It calls Opus to reason about a complex state migration. The model hallucinates a resource dependency. The agent re-plans, calls the model again for clarification, gets another hallucination, retries with more context (bigger prompt, more tokens), and enters a loop. Each iteration costs more than the last because the context window keeps growing.&lt;/p>
&lt;p>Without a gateway: you find out when the invoice arrives. $2,000 spent on a conversation with itself.&lt;/p>
&lt;p>With agentgateway: the agent hits a token-per-minute ceiling after the third iteration. The request is rejected. You get an alert. You investigate. Total damage: $12.&lt;/p>
&lt;p>That&amp;rsquo;s not a hypothetical. That&amp;rsquo;s Tuesday.&lt;/p>
&lt;h3 id="kill-switch">Kill Switch&lt;/h3>
&lt;p>agentgateway gives me a single point where I can shut everything down. If I see an agent misbehaving — through the metrics, through the traces, through an alert — I can:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Revoke the JWT&lt;/strong> for that specific agent&amp;rsquo;s identity. Immediate. That agent can&amp;rsquo;t make another LLM call or tool invocation.&lt;/li>
&lt;li>&lt;strong>Update the rate limit&lt;/strong> to zero for that agent class. Every security agent stops. Every cloud agent stops. Surgical.&lt;/li>
&lt;li>&lt;strong>Pull the gateway entirely.&lt;/strong> Nuclear option. Everything stops. Nothing reaches any LLM or tool.&lt;/li>
&lt;/ol>
&lt;p>Without a gateway, killing a rogue agent means finding the pod, kubectl exec-ing into the right node, and hoping you&amp;rsquo;re faster than the agent. With a gateway running in Kubernetes, it&amp;rsquo;s a config change — or a &lt;code>kubectl rollout restart&lt;/code> away from a full reset.&lt;/p>
&lt;h3 id="cost-controls">Cost Controls&lt;/h3>
&lt;p>Every agent has a budget. Not a suggestion — a hard limit enforced at the gateway level.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Security agent route — Opus workloads&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Cloud agent route — higher throughput&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># General agent route — simple ops&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Each route gets a token-bucket rate limit scoped by the route&amp;rsquo;s identity. The security agent running Opus gets 50k tokens per minute. That&amp;rsquo;s enough for serious threat analysis but not enough to bankrupt me on a hallucination loop. The general agent on Haiku gets 20k — simple ops don&amp;rsquo;t need more.&lt;/p>
&lt;p>agentgateway tracks token usage per provider and per model with &lt;code>agentgateway_gen_ai_client_token_usage&lt;/code> metrics, tagged with provider, model, and operation labels. I know exactly what each agent costs, in real time.&lt;/p>
&lt;h3 id="rate-limiting">Rate Limiting&lt;/h3>
&lt;p>Rate limits aren&amp;rsquo;t just about cost. They&amp;rsquo;re about preventing an agent from overwhelming a downstream system.&lt;/p>
&lt;p>A network agent running &lt;code>nmap&lt;/code> scans through an MCP tool server could, in theory, scan your entire /16 network if nobody stops it. Rate limiting at the gateway means the agent gets N tool calls per minute, period. It can&amp;rsquo;t outrun the limit no matter how convinced it is that it needs to scan &amp;ldquo;just one more subnet.&amp;rdquo;&lt;/p>
&lt;p>Same for LLM calls. An agent that retries on every 429 or timeout — something LLM providers actually rate-limit you for — gets its retries throttled at the gateway before the provider even sees them.&lt;/p>
&lt;h3 id="governance-and-rbac">Governance and RBAC&lt;/h3>
&lt;p>Each agent has a JWT identity with scoped permissions. The security agent can call &lt;code>nmap&lt;/code> and &lt;code>trivy&lt;/code> tools but cannot call &lt;code>terraform apply&lt;/code>. The cloud agent can call &lt;code>terraform plan&lt;/code> but not &lt;code>ssh&lt;/code>. The general agent can SSH to designated hosts but cannot touch cloud credentials.&lt;/p>
&lt;p>This is enforced at the gateway with CEL expressions in &lt;code>mcpAuthorization&lt;/code> rules:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Security agent backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mcpAuthorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> jwt.agent_role == &amp;#34;security&amp;#34; &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;nmap&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;trivy&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;falco&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Cloud agent backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mcpAuthorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> jwt.agent_role == &amp;#34;cloud&amp;#34; &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;terraform&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;kubectl&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;istioctl&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;helm&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Even if a specialist agent&amp;rsquo;s system prompt gets jailbroken and it tries to invoke tools outside its domain, the gateway blocks it. If a tool isn&amp;rsquo;t matched by a rule, it&amp;rsquo;s automatically filtered from the &lt;code>tools/list&lt;/code> response — the agent literally cannot see tools it doesn&amp;rsquo;t have access to.&lt;/p>
&lt;p>And since unmatched tools are denied by default, I only whitelist what&amp;rsquo;s explicitly allowed. Any tool not covered by an &lt;code>mcpAuthorization&lt;/code> rule is invisible to the agent. For HTTP-level operations, I add explicit deny rules:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">deny&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.path.contains(&amp;#34;delete&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">deny&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.path.contains(&amp;#34;destroy&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">deny&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.path.contains(&amp;#34;drop&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No agent gets to run destructive operations without explicit human escalation. Period.&lt;/p>
&lt;h3 id="full-observability">Full Observability&lt;/h3>
&lt;p>Every LLM call and every tool invocation generates OpenTelemetry traces. Every trace is tagged with the agent identity that triggered it.&lt;/p>
&lt;p>I can see:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Which agent&lt;/strong> made the call&lt;/li>
&lt;li>&lt;strong>What prompt&lt;/strong> was sent to the LLM&lt;/li>
&lt;li>&lt;strong>What tool&lt;/strong> was invoked with what arguments&lt;/li>
&lt;li>&lt;strong>How many tokens&lt;/strong> were consumed&lt;/li>
&lt;li>&lt;strong>How long&lt;/strong> it took&lt;/li>
&lt;li>&lt;strong>Whether it succeeded&lt;/strong> or failed&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌─&lt;/span> &lt;span class="n">Trace&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">security&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cve&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span> &lt;span class="err">──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">initialize&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="n">ms&lt;/span> &lt;span class="n">mcp&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">setup&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">list_tools&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="n">ms&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">discovery&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">call_tool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nmap&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">4.2&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">scan&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">target&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">host&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">llm_call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">opus&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">3.1&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">analyze&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">results&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">call_tool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">trivy&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">6.8&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">container&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">vuln&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">llm_call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">opus&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">2.4&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">synthesize&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">findings&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Total&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">16.5&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">Tokens&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">847&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">Cost&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">$&lt;/span>&lt;span class="mf">0.38&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└─────────────────────────────────────────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Metrics go to Prometheus. Traces go to Jaeger. LLM-specific telemetry goes to Langfuse for prompt/completion pair analysis. All of it through agentgateway&amp;rsquo;s built-in OpenTelemetry support — no instrumentation code in the agents themselves.&lt;/p>
&lt;p>When something goes wrong, I don&amp;rsquo;t grep through logs hoping to find what happened. I open a dashboard and see exactly which agent, which call, which tool, at what time, with what parameters.&lt;/p>
&lt;hr>
&lt;h2 id="design-decisions">Design Decisions&lt;/h2>
&lt;p>&lt;strong>Specialists are stateless, spawned per task.&lt;/strong> Simple and cost-effective. No long-running agent processes consuming resources while idle. The coordinator is the only persistent component.&lt;/p>
&lt;p>&lt;strong>Coordinator owns all memory.&lt;/strong> Specialists get context injected per request. They don&amp;rsquo;t need to remember previous conversations — the coordinator handles continuity.&lt;/p>
&lt;p>&lt;strong>Model per agent.&lt;/strong> Opus for security (high-stakes reasoning). Sonnet for network/cloud (speed + capability balance). Haiku for simple ops (cost efficiency). Each agent gets the cheapest model that&amp;rsquo;s good enough for its domain.&lt;/p>
&lt;p>&lt;strong>Tool isolation.&lt;/strong> Each specialist only gets the tools it needs. Not through prompt instructions (which can be jailbroken) but through gateway-enforced RBAC (which can&amp;rsquo;t).&lt;/p>
&lt;p>&lt;strong>Single gateway for all traffic.&lt;/strong> Not one gateway per agent. Not a sidecar pattern. One agentgateway instance running in Kubernetes that every agent routes through. One place to set policy, one place to monitor, one place to kill. K8s gives me rolling updates, health checks, and resource limits on the gateway itself — so the control plane has its own control plane.&lt;/p>
&lt;p>&lt;strong>Extensible.&lt;/strong> New domain = new agent config + system prompt + tool set. The coordinator&amp;rsquo;s routing logic gets a new keyword match. The gateway gets a new JWT scope. No architectural changes needed.&lt;/p>
&lt;hr>
&lt;h2 id="running-agentgateway-in-kubernetes">Running agentgateway in Kubernetes&lt;/h2>
&lt;p>agentgateway runs as a deployment in my Kubernetes cluster. This isn&amp;rsquo;t just convenience — it&amp;rsquo;s operational discipline. The gateway that controls all my agents is itself managed by K8s primitives: health checks, resource limits, rolling updates, and restart policies.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent-infra&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/agentgateway/agentgateway:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">admin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;128Mi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;100m&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;512Mi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;500m&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/healthz/ready&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent-infra&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">admin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The kill switch becomes even simpler in K8s. Scale to zero replicas and every agent loses its gateway instantly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale deployment agentgateway -n agent-infra --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything stops. Scale back up when you&amp;rsquo;ve fixed the issue. The gateway comes back with the same config, same policies, same state.&lt;/p>
&lt;p>The cloud agent — the one that handles Kubernetes, Istio, and ambient mesh — is particularly interesting in this setup. It manages the same cluster that hosts the gateway. That&amp;rsquo;s a circular dependency I&amp;rsquo;ve thought carefully about: the agent that manages K8s infrastructure talks through a gateway that runs on K8s infrastructure. The circuit breaker here is the RBAC policy — the cloud agent&amp;rsquo;s JWT scope explicitly excludes the &lt;code>agent-infra&lt;/code> namespace. It can manage workloads, configure Istio routing, and deploy ambient mesh policies, but it cannot touch the gateway deployment itself.&lt;/p>
&lt;hr>
&lt;h2 id="why-agentgateway-and-not-a-traditional-proxy">Why agentgateway and Not a Traditional Proxy&lt;/h2>
&lt;p>Traditional API gateways (Envoy, Kong, NGINX) were built for HTTP request/response. AI agent traffic is fundamentally different:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP is stateful.&lt;/strong> Agents maintain long-lived sessions with tool servers. Requests and responses are tied to session context. Traditional gateways don&amp;rsquo;t maintain session awareness.&lt;/li>
&lt;li>&lt;strong>LLM calls are long-running.&lt;/strong> A single inference call can take 30+ seconds with streaming. Connection timeouts designed for web APIs don&amp;rsquo;t apply.&lt;/li>
&lt;li>&lt;strong>Token-based economics.&lt;/strong> Cost isn&amp;rsquo;t about request count — it&amp;rsquo;s about token count. A gateway that can&amp;rsquo;t count tokens can&amp;rsquo;t enforce budgets.&lt;/li>
&lt;li>&lt;strong>Bidirectional communication.&lt;/strong> MCP servers can push messages back to clients asynchronously. This breaks the request/response model traditional gateways assume.&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a> is purpose-built for this. Written in Rust for performance and memory safety on stateful, long-lived connections. Understands MCP sessions natively. Counts tokens per-provider. Handles fan-out patterns where one agent call becomes multiple downstream requests.&lt;/p>
&lt;p>It&amp;rsquo;s open source, Apache 2.0 licensed, and part of the Linux Foundation. No vendor lock-in.&lt;/p>
&lt;hr>
&lt;h2 id="the-takeaway">The Takeaway&lt;/h2>
&lt;p>A multi-agent system without a control plane is a liability. Every agent you deploy is a potential cost bomb, a potential security breach, a potential &amp;ldquo;I can&amp;rsquo;t believe nobody caught that&amp;rdquo; incident.&lt;/p>
&lt;p>The architecture is straightforward:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>One coordinator&lt;/strong> that handles users and routes tasks&lt;/li>
&lt;li>&lt;strong>Specialist agents&lt;/strong> that are stateless, scoped, and disposable&lt;/li>
&lt;li>&lt;strong>One gateway&lt;/strong> that sees everything, controls everything, and logs everything&lt;/li>
&lt;/ol>
&lt;p>The coordinator decides &lt;em>what&lt;/em> gets done. The gateway decides &lt;em>whether&lt;/em> it&amp;rsquo;s allowed to happen. That separation is what makes the system safe to run autonomously.&lt;/p>
&lt;p>agentgateway isn&amp;rsquo;t optional in this architecture. It&amp;rsquo;s the thing that makes the entire system possible without me staring at a terminal 24/7 wondering if an agent is about to do something catastrophic.&lt;/p>
&lt;p>Build the agents. Put the gateway in front. Sleep at night.&lt;/p>
&lt;p>&lt;strong>Resources:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/standalone/latest/about/introduction/">agentgateway Docs — MCP&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/standalone/latest/tutorials/telemetry/">agentgateway Telemetry &amp;amp; Observability&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h2 id="the-setup">The Setup&lt;/h2>
&lt;p>I run a multi-agent system. One coordinator agent handles user interaction, memory, and routing. Specialist sub-agents get spawned on demand for domain-specific tasks — security audits, network diagnostics, cloud management, infrastructure automation. Each specialist has its own system prompt, its own toolset, and runs on a different model.&lt;/p>
&lt;p>It works. The specialists are good at their jobs. The coordinator knows when to delegate and when to handle things itself.&lt;/p>
&lt;p>But here&amp;rsquo;s what keeps me up at night: &lt;strong>what happens when one of these agents goes rogue?&lt;/strong>&lt;/p>
&lt;p>A security agent with access to nmap and trivy decides to scan every host on the network in a loop. A cloud agent burns through $500 of Opus tokens chasing a hallucinated Terraform state or decides to reconfigure your Istio ambient mesh routing because it misread a waypoint proxy status. A general agent with SSH access starts &amp;ldquo;fixing&amp;rdquo; things on production hosts that don&amp;rsquo;t need fixing.&lt;/p>
&lt;p>Without a control plane between your agents and the outside world, you have no way to stop any of this. No kill switch. No cost ceiling. No audit trail. No rate limits. Just agents with direct access to LLMs and tools, hoping nothing goes wrong.&lt;/p>
&lt;p>That&amp;rsquo;s not engineering. That&amp;rsquo;s negligence.&lt;/p>
&lt;hr>
&lt;h2 id="the-architecture">The Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s what I actually run. Every LLM call and every MCP tool invocation from every agent — coordinator and specialists alike — routes through &lt;a href="https://agentgateway.dev">agentgateway&lt;/a>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ User (Seb) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Telegram / Discord / CLI │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────────────────────┬────────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌─────────────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Coordinator Agent │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (Jacob) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • User interaction &amp;amp; conversation │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Memory management (MEMORY.md) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Task triage &amp;amp; routing │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Context assembly for specialists │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ • Result synthesis &amp;amp; delivery │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──┬──────────┬──────────┬──────────┬─────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼ ▼ ▼ ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Sec │ │ Net │ │Cloud │ │ General │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│Agent │ │Agent │ │Agent │ │ Agent │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──┬───┘ └──┬───┘ └──┬───┘ └────┬─────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┴─────────┴────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ALL traffic
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌──────────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Kubernetes Cluster │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ┌──────────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ agentgateway │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ (Pod) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Kill switch │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Rate limiting │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Cost controls │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • JWT auth + RBAC │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Observability (OTel) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ • Tool poisoning protection │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ └──────┬───────────┬───────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┼───────────┼────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────▼────┐ ┌───▼────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ LLMs │ │ MCP Servers │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Anthropic│ │ nmap, trivy │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenAI │ │ aws-cli, kubectl │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ xAI │ │ istioctl, docker │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┘ └───────────────────-┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nothing reaches an LLM or a tool without passing through the gateway. That&amp;rsquo;s the entire point.&lt;/p>
&lt;hr>
&lt;h2 id="the-agents">The Agents&lt;/h2>
&lt;h3 id="coordinator-jacob">Coordinator: Jacob&lt;/h3>
&lt;p>The coordinator is the only agent that talks to the user. It owns the conversation, manages memory (a persistent &lt;code>MEMORY.md&lt;/code> that carries context across sessions), and decides which specialist to invoke for each request.&lt;/p>
&lt;p>When a task comes in, the coordinator classifies it and builds a context payload — the relevant portion of memory, the specific question, any constraints — and spawns a specialist. The specialist does its work, returns a result, and dies. Stateless. Disposable.&lt;/p>
&lt;p>The coordinator synthesizes the result and delivers it back to the user. If a task spans multiple domains, the coordinator fans out to multiple specialists in parallel.&lt;/p>
&lt;p>&lt;strong>Model&lt;/strong>: Sonnet — fast enough for routing, smart enough for context assembly.&lt;/p>
&lt;h3 id="security-agent">Security Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: Vulnerability scanning, CVE analysis, firewall rules, IAM audits, compliance checks&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: nmap, trivy, falco, OWASP ZAP, CIS benchmarks, secrets scanning&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Opus — high reasoning for threat analysis&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Read-only on infra by default, escalation required for remediation&lt;/li>
&lt;/ul>
&lt;h3 id="network-agent">Network Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: DNS, routing, load balancing, VPN, firewall config, traffic analysis&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: dig, traceroute, tcpdump, iperf3, netstat, ip, iptables, tshark&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Sonnet — fast, good for diagnostic tasks&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Network interfaces, DNS servers, routing tables&lt;/li>
&lt;/ul>
&lt;h3 id="cloud-agent">Cloud Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: AWS/GCP/Azure resource management, Terraform, cost optimization, architecture, Kubernetes, Istio service mesh, ambient mesh&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: aws-cli, gcloud, az, terraform, kubectl, helm, istioctl&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Sonnet — balance of speed and capability&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Cloud provider credentials (scoped IAM roles), Kubernetes clusters, Istio control plane&lt;/li>
&lt;/ul>
&lt;h3 id="general--infra-agent">General / Infra Agent&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Domain&lt;/strong>: Proxmox, Docker, Linux admin, Git, CI/CD, general automation&lt;/li>
&lt;li>&lt;strong>Tools&lt;/strong>: ssh, docker, git, systemctl, proxmox API, cron&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: Sonnet (routine ops) or Haiku (simple tasks)&lt;/li>
&lt;li>&lt;strong>Access&lt;/strong>: Full local system, Proxmox API, SSH to hosts&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="routing-logic">Routing Logic&lt;/h2>
&lt;p>The coordinator classifies each request and routes to the appropriate specialist:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Keywords&lt;/th>
&lt;th>Routes To&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>CVE, vulnerability, audit, compliance, secrets&lt;/td>
&lt;td>Security Agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DNS, firewall, routing, VPN, latency, ports&lt;/td>
&lt;td>Network Agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>AWS, Terraform, GCP, Azure, S3, EC2, cost, Istio, mesh, Kubernetes, k8s&lt;/td>
&lt;td>Cloud Agent&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>VM, Docker, git, systemd, Proxmox, backup&lt;/td>
&lt;td>General Agent&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Ambiguous requests stay with the coordinator. Multi-domain tasks fan out to multiple specialists in parallel.&lt;/p>
&lt;hr>
&lt;h2 id="why-every-agent-goes-through-the-gateway">Why Every Agent Goes Through the Gateway&lt;/h2>
&lt;p>This is the part that matters. Here&amp;rsquo;s why I don&amp;rsquo;t let any agent — not even the coordinator — talk to LLMs or tools directly.&lt;/p>
&lt;h3 id="the-doom-scenario">The Doom Scenario&lt;/h3>
&lt;p>Picture this: your cloud agent is debugging a Terraform plan. It calls Opus to reason about a complex state migration. The model hallucinates a resource dependency. The agent re-plans, calls the model again for clarification, gets another hallucination, retries with more context (bigger prompt, more tokens), and enters a loop. Each iteration costs more than the last because the context window keeps growing.&lt;/p>
&lt;p>Without a gateway: you find out when the invoice arrives. $2,000 spent on a conversation with itself.&lt;/p>
&lt;p>With agentgateway: the agent hits a token-per-minute ceiling after the third iteration. The request is rejected. You get an alert. You investigate. Total damage: $12.&lt;/p>
&lt;p>That&amp;rsquo;s not a hypothetical. That&amp;rsquo;s Tuesday.&lt;/p>
&lt;h3 id="kill-switch">Kill Switch&lt;/h3>
&lt;p>agentgateway gives me a single point where I can shut everything down. If I see an agent misbehaving — through the metrics, through the traces, through an alert — I can:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Revoke the JWT&lt;/strong> for that specific agent&amp;rsquo;s identity. Immediate. That agent can&amp;rsquo;t make another LLM call or tool invocation.&lt;/li>
&lt;li>&lt;strong>Update the rate limit&lt;/strong> to zero for that agent class. Every security agent stops. Every cloud agent stops. Surgical.&lt;/li>
&lt;li>&lt;strong>Pull the gateway entirely.&lt;/strong> Nuclear option. Everything stops. Nothing reaches any LLM or tool.&lt;/li>
&lt;/ol>
&lt;p>Without a gateway, killing a rogue agent means finding the pod, kubectl exec-ing into the right node, and hoping you&amp;rsquo;re faster than the agent. With a gateway running in Kubernetes, it&amp;rsquo;s a config change — or a &lt;code>kubectl rollout restart&lt;/code> away from a full reset.&lt;/p>
&lt;h3 id="cost-controls">Cost Controls&lt;/h3>
&lt;p>Every agent has a budget. Not a suggestion — a hard limit enforced at the gateway level.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Security agent route — Opus workloads&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Cloud agent route — higher throughput&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># General agent route — simple ops&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">localRateLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tokens&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokensPerFill&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">requests&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Each route gets a token-bucket rate limit scoped by the route&amp;rsquo;s identity. The security agent running Opus gets 50k tokens per minute. That&amp;rsquo;s enough for serious threat analysis but not enough to bankrupt me on a hallucination loop. The general agent on Haiku gets 20k — simple ops don&amp;rsquo;t need more.&lt;/p>
&lt;p>agentgateway tracks token usage per provider and per model with &lt;code>agentgateway_gen_ai_client_token_usage&lt;/code> metrics, tagged with provider, model, and operation labels. I know exactly what each agent costs, in real time.&lt;/p>
&lt;h3 id="rate-limiting">Rate Limiting&lt;/h3>
&lt;p>Rate limits aren&amp;rsquo;t just about cost. They&amp;rsquo;re about preventing an agent from overwhelming a downstream system.&lt;/p>
&lt;p>A network agent running &lt;code>nmap&lt;/code> scans through an MCP tool server could, in theory, scan your entire /16 network if nobody stops it. Rate limiting at the gateway means the agent gets N tool calls per minute, period. It can&amp;rsquo;t outrun the limit no matter how convinced it is that it needs to scan &amp;ldquo;just one more subnet.&amp;rdquo;&lt;/p>
&lt;p>Same for LLM calls. An agent that retries on every 429 or timeout — something LLM providers actually rate-limit you for — gets its retries throttled at the gateway before the provider even sees them.&lt;/p>
&lt;h3 id="governance-and-rbac">Governance and RBAC&lt;/h3>
&lt;p>Each agent has a JWT identity with scoped permissions. The security agent can call &lt;code>nmap&lt;/code> and &lt;code>trivy&lt;/code> tools but cannot call &lt;code>terraform apply&lt;/code>. The cloud agent can call &lt;code>terraform plan&lt;/code> but not &lt;code>ssh&lt;/code>. The general agent can SSH to designated hosts but cannot touch cloud credentials.&lt;/p>
&lt;p>This is enforced at the gateway with CEL expressions in &lt;code>mcpAuthorization&lt;/code> rules:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Security agent backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mcpAuthorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> jwt.agent_role == &amp;#34;security&amp;#34; &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;nmap&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;trivy&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;falco&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Cloud agent backend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">mcpAuthorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> jwt.agent_role == &amp;#34;cloud&amp;#34; &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;terraform&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;kubectl&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;istioctl&amp;#34;) ||
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> mcp.tool.name.startsWith(&amp;#34;helm&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Even if a specialist agent&amp;rsquo;s system prompt gets jailbroken and it tries to invoke tools outside its domain, the gateway blocks it. If a tool isn&amp;rsquo;t matched by a rule, it&amp;rsquo;s automatically filtered from the &lt;code>tools/list&lt;/code> response — the agent literally cannot see tools it doesn&amp;rsquo;t have access to.&lt;/p>
&lt;p>And since unmatched tools are denied by default, I only whitelist what&amp;rsquo;s explicitly allowed. Any tool not covered by an &lt;code>mcpAuthorization&lt;/code> rule is invisible to the agent. For HTTP-level operations, I add explicit deny rules:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">deny&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.path.contains(&amp;#34;delete&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">deny&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.path.contains(&amp;#34;destroy&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">deny&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;request.path.contains(&amp;#34;drop&amp;#34;)&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>No agent gets to run destructive operations without explicit human escalation. Period.&lt;/p>
&lt;h3 id="full-observability">Full Observability&lt;/h3>
&lt;p>Every LLM call and every tool invocation generates OpenTelemetry traces. Every trace is tagged with the agent identity that triggered it.&lt;/p>
&lt;p>I can see:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Which agent&lt;/strong> made the call&lt;/li>
&lt;li>&lt;strong>What prompt&lt;/strong> was sent to the LLM&lt;/li>
&lt;li>&lt;strong>What tool&lt;/strong> was invoked with what arguments&lt;/li>
&lt;li>&lt;strong>How many tokens&lt;/strong> were consumed&lt;/li>
&lt;li>&lt;strong>How long&lt;/strong> it took&lt;/li>
&lt;li>&lt;strong>Whether it succeeded&lt;/strong> or failed&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="err">┌─&lt;/span> &lt;span class="n">Trace&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">security&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">agent&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cve&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span> &lt;span class="err">──────────────────┐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">initialize&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="n">ms&lt;/span> &lt;span class="n">mcp&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">session&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">setup&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">list_tools&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="n">ms&lt;/span> &lt;span class="k">tool&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">discovery&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">call_tool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">nmap&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">4.2&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">scan&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">target&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">host&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">llm_call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">opus&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">3.1&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">analyze&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">results&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">call_tool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">trivy&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">6.8&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">container&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">vuln&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">scan&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">llm_call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">opus&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="mf">2.4&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="n">synthesize&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">findings&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">│&lt;/span> &lt;span class="n">Total&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">16.5&lt;/span>&lt;span class="n">s&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">Tokens&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">12&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="mi">847&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="n">Cost&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="o">$&lt;/span>&lt;span class="mf">0.38&lt;/span> &lt;span class="err">│&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">└─────────────────────────────────────────────────────┘&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Metrics go to Prometheus. Traces go to Jaeger. LLM-specific telemetry goes to Langfuse for prompt/completion pair analysis. All of it through agentgateway&amp;rsquo;s built-in OpenTelemetry support — no instrumentation code in the agents themselves.&lt;/p>
&lt;p>When something goes wrong, I don&amp;rsquo;t grep through logs hoping to find what happened. I open a dashboard and see exactly which agent, which call, which tool, at what time, with what parameters.&lt;/p>
&lt;hr>
&lt;h2 id="design-decisions">Design Decisions&lt;/h2>
&lt;p>&lt;strong>Specialists are stateless, spawned per task.&lt;/strong> Simple and cost-effective. No long-running agent processes consuming resources while idle. The coordinator is the only persistent component.&lt;/p>
&lt;p>&lt;strong>Coordinator owns all memory.&lt;/strong> Specialists get context injected per request. They don&amp;rsquo;t need to remember previous conversations — the coordinator handles continuity.&lt;/p>
&lt;p>&lt;strong>Model per agent.&lt;/strong> Opus for security (high-stakes reasoning). Sonnet for network/cloud (speed + capability balance). Haiku for simple ops (cost efficiency). Each agent gets the cheapest model that&amp;rsquo;s good enough for its domain.&lt;/p>
&lt;p>&lt;strong>Tool isolation.&lt;/strong> Each specialist only gets the tools it needs. Not through prompt instructions (which can be jailbroken) but through gateway-enforced RBAC (which can&amp;rsquo;t).&lt;/p>
&lt;p>&lt;strong>Single gateway for all traffic.&lt;/strong> Not one gateway per agent. Not a sidecar pattern. One agentgateway instance running in Kubernetes that every agent routes through. One place to set policy, one place to monitor, one place to kill. K8s gives me rolling updates, health checks, and resource limits on the gateway itself — so the control plane has its own control plane.&lt;/p>
&lt;p>&lt;strong>Extensible.&lt;/strong> New domain = new agent config + system prompt + tool set. The coordinator&amp;rsquo;s routing logic gets a new keyword match. The gateway gets a new JWT scope. No architectural changes needed.&lt;/p>
&lt;hr>
&lt;h2 id="running-agentgateway-in-kubernetes">Running agentgateway in Kubernetes&lt;/h2>
&lt;p>agentgateway runs as a deployment in my Kubernetes cluster. This isn&amp;rsquo;t just convenience — it&amp;rsquo;s operational discipline. The gateway that controls all my agents is itself managed by K8s primitives: health checks, resource limits, rolling updates, and restart policies.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent-infra&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ghcr.io/agentgateway/agentgateway:latest&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">admin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;128Mi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;100m&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;512Mi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;500m&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">readinessProbe&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/healthz/ready&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">initialDelaySeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/etc/agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agent-infra&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">admin&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15020&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The kill switch becomes even simpler in K8s. Scale to zero replicas and every agent loses its gateway instantly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl scale deployment agentgateway -n agent-infra --replicas&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Everything stops. Scale back up when you&amp;rsquo;ve fixed the issue. The gateway comes back with the same config, same policies, same state.&lt;/p>
&lt;p>The cloud agent — the one that handles Kubernetes, Istio, and ambient mesh — is particularly interesting in this setup. It manages the same cluster that hosts the gateway. That&amp;rsquo;s a circular dependency I&amp;rsquo;ve thought carefully about: the agent that manages K8s infrastructure talks through a gateway that runs on K8s infrastructure. The circuit breaker here is the RBAC policy — the cloud agent&amp;rsquo;s JWT scope explicitly excludes the &lt;code>agent-infra&lt;/code> namespace. It can manage workloads, configure Istio routing, and deploy ambient mesh policies, but it cannot touch the gateway deployment itself.&lt;/p>
&lt;hr>
&lt;h2 id="why-agentgateway-and-not-a-traditional-proxy">Why agentgateway and Not a Traditional Proxy&lt;/h2>
&lt;p>Traditional API gateways (Envoy, Kong, NGINX) were built for HTTP request/response. AI agent traffic is fundamentally different:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MCP is stateful.&lt;/strong> Agents maintain long-lived sessions with tool servers. Requests and responses are tied to session context. Traditional gateways don&amp;rsquo;t maintain session awareness.&lt;/li>
&lt;li>&lt;strong>LLM calls are long-running.&lt;/strong> A single inference call can take 30+ seconds with streaming. Connection timeouts designed for web APIs don&amp;rsquo;t apply.&lt;/li>
&lt;li>&lt;strong>Token-based economics.&lt;/strong> Cost isn&amp;rsquo;t about request count — it&amp;rsquo;s about token count. A gateway that can&amp;rsquo;t count tokens can&amp;rsquo;t enforce budgets.&lt;/li>
&lt;li>&lt;strong>Bidirectional communication.&lt;/strong> MCP servers can push messages back to clients asynchronously. This breaks the request/response model traditional gateways assume.&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a> is purpose-built for this. Written in Rust for performance and memory safety on stateful, long-lived connections. Understands MCP sessions natively. Counts tokens per-provider. Handles fan-out patterns where one agent call becomes multiple downstream requests.&lt;/p>
&lt;p>It&amp;rsquo;s open source, Apache 2.0 licensed, and part of the Linux Foundation. No vendor lock-in.&lt;/p>
&lt;hr>
&lt;h2 id="the-takeaway">The Takeaway&lt;/h2>
&lt;p>A multi-agent system without a control plane is a liability. Every agent you deploy is a potential cost bomb, a potential security breach, a potential &amp;ldquo;I can&amp;rsquo;t believe nobody caught that&amp;rdquo; incident.&lt;/p>
&lt;p>The architecture is straightforward:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>One coordinator&lt;/strong> that handles users and routes tasks&lt;/li>
&lt;li>&lt;strong>Specialist agents&lt;/strong> that are stateless, scoped, and disposable&lt;/li>
&lt;li>&lt;strong>One gateway&lt;/strong> that sees everything, controls everything, and logs everything&lt;/li>
&lt;/ol>
&lt;p>The coordinator decides &lt;em>what&lt;/em> gets done. The gateway decides &lt;em>whether&lt;/em> it&amp;rsquo;s allowed to happen. That separation is what makes the system safe to run autonomously.&lt;/p>
&lt;p>agentgateway isn&amp;rsquo;t optional in this architecture. It&amp;rsquo;s the thing that makes the entire system possible without me staring at a terminal 24/7 wondering if an agent is about to do something catastrophic.&lt;/p>
&lt;p>Build the agents. Put the gateway in front. Sleep at night.&lt;/p>
&lt;p>&lt;strong>Resources:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/standalone/latest/about/introduction/">agentgateway Docs — MCP&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/standalone/latest/tutorials/telemetry/">agentgateway Telemetry &amp;amp; Observability&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>MCP Multiplexing with agentgateway</title><link>https://maniak.io/articles/2026-02-20-mcp-multiplexing-tool-access-agentgateway/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-20-mcp-multiplexing-tool-access-agentgateway/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>As your agentic AI environment grows, you end up with a sprawl of MCP servers — one for time utilities, one for general-purpose tools, one for Slack, another for GitHub. Each server exposes different tools, runs on a different port, and needs its own connection configuration.&lt;/p>
&lt;p>Your MCP clients (Cursor, Claude Desktop, VS Code) shouldn&amp;rsquo;t need to know about every server individually. They should connect to &lt;strong>one endpoint&lt;/strong> and see &lt;strong>all available tools&lt;/strong>.&lt;/p>
&lt;p>This guide walks you through &lt;strong>MCP Multiplexing&lt;/strong> — federating multiple MCP servers behind a single agentgateway endpoint.&lt;/p>
&lt;p>We&amp;rsquo;ll deploy two MCP servers (&lt;code>mcp-server-everything&lt;/code> for utility tools and &lt;code>mcp-website-fetcher&lt;/code> for fetching web content), multiplex them behind agentgateway, and connect from any IDE.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>One endpoint, many servers&lt;/strong>: Clients connect to &lt;code>/mcp&lt;/code> and see tools from all federated servers&lt;/li>
&lt;li>&lt;strong>Automatic tool namespacing&lt;/strong>: Tools are prefixed with the server name to avoid conflicts&lt;/li>
&lt;li>&lt;strong>Label-based federation&lt;/strong>: Add servers by just adding a Kubernetes label — no config changes&lt;/li>
&lt;li>&lt;strong>IDE-agnostic&lt;/strong>: Works with Cursor, VS Code, Claude Code, Windsurf, OpenCode&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Cursor │──┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ VS Code │──┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ ┌──────────────────┐ ┌──────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code │──┼─MCP──▶│ agentgateway │──────▶│ mcp-server-everything│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ │ /mcp endpoint │ │ (echo, add, etc.) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Windsurf │──┘ │ │──────▶├──────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ │ Multiplexing │ │ mcp-website-fetcher │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────┘ │ (fetch web content) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind&lt;/a> installed&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code> and &lt;code>helm&lt;/code> installed&lt;/li>
&lt;li>At least one MCP client: Cursor, VS Code, Claude Code, Windsurf, or OpenCode&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-agentgateway-oss">Step 2: Install agentgateway OSS&lt;/h2>
&lt;p>Deploy the Kubernetes Gateway API CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the agentgateway CRDs and control plane:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway-crds oci://ghcr.io/kgateway-dev/charts/agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify the control plane is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see the &lt;code>agentgateway&lt;/code> control plane pod in &lt;code>Running&lt;/code> state.&lt;/p>
&lt;h2 id="step-3-create-an-agentgateway-proxy">Step 3: Create an agentgateway Proxy&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for the proxy pod:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/mcp-gateway -n agentgateway-system --timeout&lt;span class="o">=&lt;/span>60s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4-deploy-two-mcp-servers">Step 4: Deploy Two MCP Servers&lt;/h2>
&lt;h3 id="server-1-mcp-server-everything">Server 1: mcp-server-everything&lt;/h3>
&lt;p>This is the reference MCP test server from the Model Context Protocol project. It provides utility tools like &lt;code>echo&lt;/code>, &lt;code>add&lt;/code>, &lt;code>longRunningOperation&lt;/code>, and more. The &lt;code>streamableHttp&lt;/code> argument tells it to listen over HTTP instead of stdio — which is exactly the transport agentgateway&amp;rsquo;s multiplex feature requires.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: node:20-alpine
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> command: [&amp;#34;npx&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args: [&amp;#34;-y&amp;#34;, &amp;#34;@modelcontextprotocol/server-everything&amp;#34;, &amp;#34;streamableHttp&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 3001
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp-federation: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 3001
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 3001
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> appProtocol: kgateway.dev/mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ClusterIP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="server-2-mcp-website-fetcher">Server 2: mcp-website-fetcher&lt;/h3>
&lt;p>This server provides a &lt;code>fetch&lt;/code> tool that can retrieve and extract content from any URL — useful for giving your AI assistant web browsing capabilities. It&amp;rsquo;s already HTTP-native, no wrapper needed.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: ConfigMap
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher-fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">data:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app.py: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> import httpx
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from mcp.server.fastmcp import FastMCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp = FastMCP(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;mcp-website-fetcher&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> host=&amp;#34;0.0.0.0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port=8000,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> streamable_http_path=&amp;#34;/mcp&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> stateless_http=True,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> @mcp.tool()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> async def fetch(url: str) -&amp;gt; str:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;&amp;#34;&amp;#34;Fetches a website and returns its content&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers = {&amp;#34;User-Agent&amp;#34;: &amp;#34;MCP Test Server&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response = await client.get(url)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.raise_for_status()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return response.text
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if __name__ == &amp;#34;__main__&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp.run(transport=&amp;#34;streamable-http&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: ghcr.io/peterj/mcp-website-fetcher:main
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> imagePullPolicy: Always
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> command: [&amp;#34;python3&amp;#34;, &amp;#34;/app/app.py&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumeMounts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mountPath: /app/app.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> subPath: app.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> configMap:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher-fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp-federation: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> appProtocol: kgateway.dev/mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ClusterIP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for both servers to be ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/mcp-server-everything -n agentgateway-system --timeout&lt;span class="o">=&lt;/span>60s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/mcp-website-fetcher -n agentgateway-system --timeout&lt;span class="o">=&lt;/span>60s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Key detail&lt;/strong>: Both services have the label &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code> and &lt;code>appProtocol: kgateway.dev/mcp&lt;/code>. This is how agentgateway discovers and federates them.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-5-create-the-multiplexed-mcp-backend">Step 5: Create the Multiplexed MCP Backend&lt;/h2>
&lt;p>Here&amp;rsquo;s where the magic happens. Instead of pointing to individual servers, you use a &lt;strong>label selector&lt;/strong> to match all services with &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-federated
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targets:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-servers
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> services:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp-federation: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This single backend automatically discovers both &lt;code>mcp-server-everything&lt;/code> and &lt;code>mcp-website-fetcher&lt;/code> — and any future MCP server you deploy with the &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code> label.&lt;/p>
&lt;h2 id="step-6-create-the-httproute">Step 6: Create the HTTPRoute&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-federated
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-7-port-forward-and-test">Step 7: Port-Forward and Test&lt;/h2>
&lt;p>Expose the gateway locally:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system deployment/mcp-gateway 8080:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-with-mcp-inspector">Test with MCP Inspector&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx @modelcontextprotocol/inspector@latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Connect with:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Transport&lt;/strong>: Streamable HTTP&lt;/li>
&lt;li>&lt;strong>URL&lt;/strong>: &lt;code>http://localhost:8080/mcp&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Click &lt;strong>Tools&lt;/strong> → &lt;strong>List Tools&lt;/strong>. You should see tools from &lt;strong>both&lt;/strong> servers, namespaced:&lt;/p>
&lt;ul>
&lt;li>&lt;code>mcp-server-everything-3001_echo&lt;/code> — echo a message&lt;/li>
&lt;li>&lt;code>mcp-server-everything-3001_add&lt;/code> — add two numbers&lt;/li>
&lt;li>&lt;code>mcp-server-everything-3001_longRunningOperation&lt;/code> — simulate a long task&lt;/li>
&lt;li>&lt;code>mcp-website-fetcher-80_fetch&lt;/code> — fetch and extract content from a URL&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://github.com/user-attachments/assets/e2a434dc-5ad0-4c74-9d63-c8b156e70219" aria-label="Open full-size image">
 &lt;img src="https://github.com/user-attachments/assets/e2a434dc-5ad0-4c74-9d63-c8b156e70219" alt="mcpsuccess" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;strong>One endpoint. Two servers. All tools.&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="step-8-connect-from-your-ide">Step 8: Connect from Your IDE&lt;/h2>
&lt;p>With port-forward running, configure your IDE:&lt;/p>
&lt;h3 id="cursor">Cursor&lt;/h3>
&lt;p>Create or edit &lt;code>~/.cursor/mcp.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="vs-code-with-github-copilot">VS Code (with GitHub Copilot)&lt;/h3>
&lt;p>Add to &lt;code>settings.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github.copilot.chat.mcp.servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="claude-code">Claude Code&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">claude mcp add federated-tools --transport sse http://localhost:8080/mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="windsurf">Windsurf&lt;/h3>
&lt;p>Create or edit &lt;code>~/.windsurf/mcp.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="opencode">OpenCode&lt;/h3>
&lt;p>Add to &lt;code>opencode.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now try asking your IDE:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;Echo back: agentgateway is awesome&amp;rdquo;&lt;/em> — uses the everything server&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Add 42 and 58&amp;rdquo;&lt;/em> — uses the everything server&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Fetch the content from &lt;a href="https://agentgateway.dev">https://agentgateway.dev&lt;/a>&amp;rdquo;&lt;/em> — uses the website fetcher&lt;/li>
&lt;/ul>
&lt;p>The IDE doesn&amp;rsquo;t know or care that these tools come from different servers. It&amp;rsquo;s all one MCP endpoint.&lt;/p>
&lt;hr>
&lt;h2 id="adding-more-mcp-servers">Adding More MCP Servers&lt;/h2>
&lt;p>The beauty of label-based federation: adding a new server is just a deployment + service with the &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code> label. No agentgateway config changes needed — the backend&amp;rsquo;s label selector picks it up automatically.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl delete httproute mcp-route -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend mcp-federated -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete deploy mcp-server-everything mcp-website-fetcher -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete svc mcp-server-everything mcp-website-fetcher -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>MCP multiplexing solves one of the biggest challenges in production agentic AI environments: &lt;strong>server sprawl&lt;/strong>. Developers shouldn&amp;rsquo;t need to configure connections to every MCP server individually. One gateway endpoint, all tools.&lt;/p>
&lt;p>Federate all your MCP servers behind agentgateway, add new servers with a label, and every connected client sees the new tools immediately. No agent code changes. No client reconfiguration.&lt;/p>
&lt;p>One gateway. All your tools.&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>As your agentic AI environment grows, you end up with a sprawl of MCP servers — one for time utilities, one for general-purpose tools, one for Slack, another for GitHub. Each server exposes different tools, runs on a different port, and needs its own connection configuration.&lt;/p>
&lt;p>Your MCP clients (Cursor, Claude Desktop, VS Code) shouldn&amp;rsquo;t need to know about every server individually. They should connect to &lt;strong>one endpoint&lt;/strong> and see &lt;strong>all available tools&lt;/strong>.&lt;/p>
&lt;p>This guide walks you through &lt;strong>MCP Multiplexing&lt;/strong> — federating multiple MCP servers behind a single agentgateway endpoint.&lt;/p>
&lt;p>We&amp;rsquo;ll deploy two MCP servers (&lt;code>mcp-server-everything&lt;/code> for utility tools and &lt;code>mcp-website-fetcher&lt;/code> for fetching web content), multiplex them behind agentgateway, and connect from any IDE.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>One endpoint, many servers&lt;/strong>: Clients connect to &lt;code>/mcp&lt;/code> and see tools from all federated servers&lt;/li>
&lt;li>&lt;strong>Automatic tool namespacing&lt;/strong>: Tools are prefixed with the server name to avoid conflicts&lt;/li>
&lt;li>&lt;strong>Label-based federation&lt;/strong>: Add servers by just adding a Kubernetes label — no config changes&lt;/li>
&lt;li>&lt;strong>IDE-agnostic&lt;/strong>: Works with Cursor, VS Code, Claude Code, Windsurf, OpenCode&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Cursor │──┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ VS Code │──┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ ┌──────────────────┐ ┌──────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code │──┼─MCP──▶│ agentgateway │──────▶│ mcp-server-everything│
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ │ /mcp endpoint │ │ (echo, add, etc.) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Windsurf │──┘ │ │──────▶├──────────────────────┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ │ Multiplexing │ │ mcp-website-fetcher │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────┘ │ (fetch web content) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind&lt;/a> installed&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code> and &lt;code>helm&lt;/code> installed&lt;/li>
&lt;li>At least one MCP client: Cursor, VS Code, Claude Code, Windsurf, or OpenCode&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-agentgateway-oss">Step 2: Install agentgateway OSS&lt;/h2>
&lt;p>Deploy the Kubernetes Gateway API CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the agentgateway CRDs and control plane:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway-crds oci://ghcr.io/kgateway-dev/charts/agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify the control plane is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see the &lt;code>agentgateway&lt;/code> control plane pod in &lt;code>Running&lt;/code> state.&lt;/p>
&lt;h2 id="step-3-create-an-agentgateway-proxy">Step 3: Create an agentgateway Proxy&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for the proxy pod:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/mcp-gateway -n agentgateway-system --timeout&lt;span class="o">=&lt;/span>60s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4-deploy-two-mcp-servers">Step 4: Deploy Two MCP Servers&lt;/h2>
&lt;h3 id="server-1-mcp-server-everything">Server 1: mcp-server-everything&lt;/h3>
&lt;p>This is the reference MCP test server from the Model Context Protocol project. It provides utility tools like &lt;code>echo&lt;/code>, &lt;code>add&lt;/code>, &lt;code>longRunningOperation&lt;/code>, and more. The &lt;code>streamableHttp&lt;/code> argument tells it to listen over HTTP instead of stdio — which is exactly the transport agentgateway&amp;rsquo;s multiplex feature requires.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: node:20-alpine
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> command: [&amp;#34;npx&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args: [&amp;#34;-y&amp;#34;, &amp;#34;@modelcontextprotocol/server-everything&amp;#34;, &amp;#34;streamableHttp&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 3001
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp-federation: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-server-everything
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 3001
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 3001
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> appProtocol: kgateway.dev/mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ClusterIP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="server-2-mcp-website-fetcher">Server 2: mcp-website-fetcher&lt;/h3>
&lt;p>This server provides a &lt;code>fetch&lt;/code> tool that can retrieve and extract content from any URL — useful for giving your AI assistant web browsing capabilities. It&amp;rsquo;s already HTTP-native, no wrapper needed.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: ConfigMap
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher-fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">data:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app.py: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> import httpx
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from mcp.server.fastmcp import FastMCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp = FastMCP(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;mcp-website-fetcher&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> host=&amp;#34;0.0.0.0&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port=8000,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> streamable_http_path=&amp;#34;/mcp&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> stateless_http=True,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> @mcp.tool()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> async def fetch(url: str) -&amp;gt; str:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;&amp;#34;&amp;#34;Fetches a website and returns its content&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers = {&amp;#34;User-Agent&amp;#34;: &amp;#34;MCP Test Server&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> async with httpx.AsyncClient(follow_redirects=True, headers=headers) as client:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response = await client.get(url)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.raise_for_status()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return response.text
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if __name__ == &amp;#34;__main__&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp.run(transport=&amp;#34;streamable-http&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: ghcr.io/peterj/mcp-website-fetcher:main
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> imagePullPolicy: Always
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> command: [&amp;#34;python3&amp;#34;, &amp;#34;/app/app.py&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumeMounts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mountPath: /app/app.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> subPath: app.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> configMap:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher-fix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp-federation: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-website-fetcher
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> appProtocol: kgateway.dev/mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ClusterIP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for both servers to be ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/mcp-server-everything -n agentgateway-system --timeout&lt;span class="o">=&lt;/span>60s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/mcp-website-fetcher -n agentgateway-system --timeout&lt;span class="o">=&lt;/span>60s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Key detail&lt;/strong>: Both services have the label &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code> and &lt;code>appProtocol: kgateway.dev/mcp&lt;/code>. This is how agentgateway discovers and federates them.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-5-create-the-multiplexed-mcp-backend">Step 5: Create the Multiplexed MCP Backend&lt;/h2>
&lt;p>Here&amp;rsquo;s where the magic happens. Instead of pointing to individual servers, you use a &lt;strong>label selector&lt;/strong> to match all services with &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-federated
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targets:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-servers
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> services:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp-federation: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This single backend automatically discovers both &lt;code>mcp-server-everything&lt;/code> and &lt;code>mcp-website-fetcher&lt;/code> — and any future MCP server you deploy with the &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code> label.&lt;/p>
&lt;h2 id="step-6-create-the-httproute">Step 6: Create the HTTPRoute&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-federated
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-7-port-forward-and-test">Step 7: Port-Forward and Test&lt;/h2>
&lt;p>Expose the gateway locally:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system deployment/mcp-gateway 8080:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-with-mcp-inspector">Test with MCP Inspector&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx @modelcontextprotocol/inspector@latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Connect with:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Transport&lt;/strong>: Streamable HTTP&lt;/li>
&lt;li>&lt;strong>URL&lt;/strong>: &lt;code>http://localhost:8080/mcp&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Click &lt;strong>Tools&lt;/strong> → &lt;strong>List Tools&lt;/strong>. You should see tools from &lt;strong>both&lt;/strong> servers, namespaced:&lt;/p>
&lt;ul>
&lt;li>&lt;code>mcp-server-everything-3001_echo&lt;/code> — echo a message&lt;/li>
&lt;li>&lt;code>mcp-server-everything-3001_add&lt;/code> — add two numbers&lt;/li>
&lt;li>&lt;code>mcp-server-everything-3001_longRunningOperation&lt;/code> — simulate a long task&lt;/li>
&lt;li>&lt;code>mcp-website-fetcher-80_fetch&lt;/code> — fetch and extract content from a URL&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://github.com/user-attachments/assets/e2a434dc-5ad0-4c74-9d63-c8b156e70219" aria-label="Open full-size image">
 &lt;img src="https://github.com/user-attachments/assets/e2a434dc-5ad0-4c74-9d63-c8b156e70219" alt="mcpsuccess" loading="lazy">
&lt;/a>
&lt;/p>
&lt;p>&lt;strong>One endpoint. Two servers. All tools.&lt;/strong>&lt;/p>
&lt;hr>
&lt;h2 id="step-8-connect-from-your-ide">Step 8: Connect from Your IDE&lt;/h2>
&lt;p>With port-forward running, configure your IDE:&lt;/p>
&lt;h3 id="cursor">Cursor&lt;/h3>
&lt;p>Create or edit &lt;code>~/.cursor/mcp.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="vs-code-with-github-copilot">VS Code (with GitHub Copilot)&lt;/h3>
&lt;p>Add to &lt;code>settings.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github.copilot.chat.mcp.servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="claude-code">Claude Code&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">claude mcp add federated-tools --transport sse http://localhost:8080/mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="windsurf">Windsurf&lt;/h3>
&lt;p>Create or edit &lt;code>~/.windsurf/mcp.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="opencode">OpenCode&lt;/h3>
&lt;p>Add to &lt;code>opencode.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;federated-tools&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now try asking your IDE:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;Echo back: agentgateway is awesome&amp;rdquo;&lt;/em> — uses the everything server&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Add 42 and 58&amp;rdquo;&lt;/em> — uses the everything server&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Fetch the content from &lt;a href="https://agentgateway.dev">https://agentgateway.dev&lt;/a>&amp;rdquo;&lt;/em> — uses the website fetcher&lt;/li>
&lt;/ul>
&lt;p>The IDE doesn&amp;rsquo;t know or care that these tools come from different servers. It&amp;rsquo;s all one MCP endpoint.&lt;/p>
&lt;hr>
&lt;h2 id="adding-more-mcp-servers">Adding More MCP Servers&lt;/h2>
&lt;p>The beauty of label-based federation: adding a new server is just a deployment + service with the &lt;code>mcp-federation: &amp;quot;true&amp;quot;&lt;/code> label. No agentgateway config changes needed — the backend&amp;rsquo;s label selector picks it up automatically.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl delete httproute mcp-route -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend mcp-federated -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete deploy mcp-server-everything mcp-website-fetcher -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete svc mcp-server-everything mcp-website-fetcher -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway-mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>MCP multiplexing solves one of the biggest challenges in production agentic AI environments: &lt;strong>server sprawl&lt;/strong>. Developers shouldn&amp;rsquo;t need to configure connections to every MCP server individually. One gateway endpoint, all tools.&lt;/p>
&lt;p>Federate all your MCP servers behind agentgateway, add new servers with a label, and every connected client sees the new tools immediately. No agent code changes. No client reconfiguration.&lt;/p>
&lt;p>One gateway. All your tools.&lt;/p></content:encoded></item><item><title>AWS AgentCore with agentgateway and Okta OAuth</title><link>https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/</link><pubDate>Thu, 19 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-19-why-your-ai-agents-need-a-gateway/</guid><description>&lt;p>You wouldn&amp;rsquo;t deploy microservices without an API gateway. So why are teams deploying AI agents with direct, ungoverned access to LLMs and external tools?&lt;/p>
&lt;p>If you&amp;rsquo;re running agents on AWS Bedrock AgentCore — or anywhere else — and those agents are calling Claude, GPT-4, or hitting MCP tool servers directly, you have a governance gap. No rate limiting. No audit trail. No PII filtering. No failover. Every team wiring up their own retry logic, their own auth handling, their own cost tracking. It&amp;rsquo;s 2016 microservices all over again, except the blast radius includes sending your customer database to a third-party LLM.&lt;/p>
&lt;p>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway&lt;/a> fixes this. It&amp;rsquo;s an open-source (CNCF) gateway purpose-built for AI agent traffic — both LLM calls and MCP tool calls — giving you a single control plane for everything your agents talk to.&lt;/p>
&lt;p>But here&amp;rsquo;s the thing: you don&amp;rsquo;t need &lt;em>another&lt;/em> gateway from your cloud provider to do it. agentgateway is your gateway — for auth, RBAC, observability, and security. Your cloud just provides compute.&lt;/p>
&lt;p>This post walks through why, how the auth chain works end-to-end, and how the &lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">aws-agentcore-demo&lt;/a> wires it all together.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem-agents-without-guardrails">The Problem: Agents Without Guardrails&lt;/h2>
&lt;p>An AI agent is, at its core, a loop: receive input → call an LLM → maybe call some tools → return output. The interesting part is what happens in those calls.&lt;/p>
&lt;p>When an agent on AgentCore calls Anthropic&amp;rsquo;s API directly:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>API keys live in the agent container.&lt;/strong> Every developer who can deploy an agent has access to production LLM credentials.&lt;/li>
&lt;li>&lt;strong>No visibility.&lt;/strong> What prompts are being sent? What data is in them? You don&amp;rsquo;t know until something leaks.&lt;/li>
&lt;li>&lt;strong>No cost controls.&lt;/strong> One runaway agent loop burns through your API budget in minutes. There&amp;rsquo;s no per-user or per-agent rate limiting.&lt;/li>
&lt;li>&lt;strong>No failover.&lt;/strong> Anthropic has an outage? Your agent is dead. Hope someone set up a retry with exponential backoff — oh wait, each team did it differently.&lt;/li>
&lt;li>&lt;strong>No security policies.&lt;/strong> PII goes straight to the LLM. Prompt injection attempts pass through unfiltered. Credentials in responses get forwarded to users.&lt;/li>
&lt;/ul>
&lt;p>And then there&amp;rsquo;s MCP. Your agent discovers tools — Slack, GitHub, internal APIs — via Model Context Protocol servers. Each MCP server needs auth. Each one is another surface to secure, another thing to monitor, another connection to manage.&lt;/p>
&lt;p>Every team solving these problems independently is wasted engineering. Worse, most teams don&amp;rsquo;t solve them at all.&lt;/p>
&lt;hr>
&lt;h2 id="the-trap-another-vendor-gateway">The Trap: Another Vendor Gateway&lt;/h2>
&lt;p>Cloud providers see this problem too. Their solution? Another managed service. AWS has AgentCore Gateway. Others will follow.&lt;/p>
&lt;p>But think about what that means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You&amp;rsquo;re locked in.&lt;/strong> That gateway only works on AWS. Move to GCP, Azure, or on-prem? Rebuild your auth, RBAC, and observability from scratch.&lt;/li>
&lt;li>&lt;strong>Double the gateways.&lt;/strong> Now you have a cloud gateway &lt;em>and&lt;/em> your actual governance layer. Two hops, two configurations, two things to debug.&lt;/li>
&lt;li>&lt;strong>Weakest-link security.&lt;/strong> If your cloud gateway does JWT validation but your real tools don&amp;rsquo;t, you have a false sense of security. If both do it, you&amp;rsquo;re paying for redundancy.&lt;/li>
&lt;li>&lt;strong>Vendor roadmap dependency.&lt;/strong> Need scope-based MCP RBAC? PII filtering? Prompt injection detection? You&amp;rsquo;re waiting on their feature backlog.&lt;/li>
&lt;/ul>
&lt;p>The right answer: use your cloud provider for what it&amp;rsquo;s good at — &lt;strong>compute&lt;/strong> — and use a purpose-built, portable gateway for &lt;strong>governance&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="the-solution-agentgateway">The Solution: agentgateway&lt;/h2>
&lt;p>agentgateway sits between your agents and everything they talk to. LLMs and MCP tool servers both route through it.&lt;/p>
&lt;p>This isn&amp;rsquo;t Envoy with some AI plugins bolted on. agentgateway is a purpose-built proxy with its own xDS-inspired control plane, designed from the ground up for agent traffic patterns. It understands:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM protocols&lt;/strong> — OpenAI-compatible chat completions, streaming, token counting&lt;/li>
&lt;li>&lt;strong>MCP protocol&lt;/strong> — tool discovery, tool invocation, server lifecycle&lt;/li>
&lt;li>&lt;strong>Agent-specific policies&lt;/strong> — PII redaction, prompt injection detection, per-user token budgets&lt;/li>
&lt;li>&lt;strong>JWT authentication&lt;/strong> — validate Okta/Entra/any OIDC tokens on MCP routes with scope-based RBAC&lt;/li>
&lt;/ul>
&lt;p>Two logical functions, one binary:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>LLM Proxy&lt;/strong> — OpenAI-compatible endpoint that routes to any provider (Anthropic, OpenAI, xAI, local models)&lt;/li>
&lt;li>&lt;strong>MCP Gateway&lt;/strong> — aggregates multiple MCP servers, presents a unified tool catalog to agents, enforces auth + RBAC&lt;/li>
&lt;/ol>
&lt;p>Your agent code barely changes. Point it at agentgateway instead of the LLM provider directly. Add a Bearer token to MCP calls. That&amp;rsquo;s it.&lt;/p>
&lt;hr>
&lt;h2 id="traffic-flow-how-this-demo-works">Traffic Flow: How This Demo Works&lt;/h2>
&lt;p>The &lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">aws-agentcore-demo&lt;/a> is a working reference architecture. Here&amp;rsquo;s what&amp;rsquo;s actually running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌───────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ AWS Bedrock AgentCore │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌───────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Agent Container │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (arm64, Python/FastAPI) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Gets Okta JWT │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (client_credentials) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └──────┬──────────┬──────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌──────▼──┐ ┌────▼─────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Runtime │ │ (no gateway) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Endpoint │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └─────────┘ └──────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────────────────────┼────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ngrok tunnels
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ k8s-rooster (on-prem k8s) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ agentgateway (Enterprise) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌──────────┐ ┌──────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ LLM │ │ MCP │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Proxy │ │ Gateway │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (no auth)│ │ (Okta JWT auth) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │ (scope RBAC) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────┬─────┘ └──────┬───────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└───────┼──────────────┼─────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────▼────┐ ┌──────▼──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Anthropic│ │ MCP Servers │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │OpenAI │ │ - Slack │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │xAI │ │ - GitHub │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┘ │ - Tools │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>The agent container&lt;/strong> runs on AgentCore as an arm64 Python/FastAPI application. It doesn&amp;rsquo;t hold any LLM API keys. It knows two endpoints: agentgateway&amp;rsquo;s LLM proxy and MCP gateway.&lt;/p>
&lt;p>&lt;strong>LLM calls&lt;/strong> go through agentgateway&amp;rsquo;s OpenAI-compatible proxy — no auth needed. agentgateway holds the API keys (stored as Kubernetes Secrets, managed by ArgoCD), applies policies, logs the interaction, and returns the response.&lt;/p>
&lt;p>&lt;strong>MCP tool calls&lt;/strong> go through agentgateway&amp;rsquo;s MCP gateway. The agent includes its Okta JWT (obtained via &lt;code>client_credentials&lt;/code> grant) in every MCP request. agentgateway validates the token, checks scopes, and enforces RBAC before the tool call reaches the upstream MCP server.&lt;/p>
&lt;p>&lt;strong>Connectivity&lt;/strong> between AWS and the on-prem k8s-rooster cluster uses ngrok tunnels — pragmatic for a demo. In production, you&amp;rsquo;d use VPC peering, PrivateLink, or similar.&lt;/p>
&lt;p>&lt;strong>No AgentCore Gateway.&lt;/strong> The agent container is invoked directly through the AgentCore Runtime endpoint. AWS IAM controls who can invoke the agent. agentgateway controls what the agent can do.&lt;/p>
&lt;hr>
&lt;h2 id="authentication-end-to-end">Authentication: End-to-End&lt;/h2>
&lt;p>Every MCP request traverses an authenticated chain — from AWS IAM through Okta to tool invocation. &lt;strong>No ambient credentials, no hardcoded API keys in agent code, no unauthenticated MCP hops.&lt;/strong>&lt;/p>
&lt;h3 id="the-auth-stack">The Auth Stack&lt;/h3>
&lt;div class="mermaid">sequenceDiagram
 participant I as Invoker
 participant IAM as AWS IAM
 participant R as AgentCore Runtime
 participant A as Agent
 participant O as Okta
 participant AG as agentgateway
 participant LLM as LLM Provider
 participant MCP as MCP Tools

 I-&amp;gt;&amp;gt;IAM: invoke-agent-runtime (SigV4)
 IAM-&amp;gt;&amp;gt;R: Authorized → forward to agent
 R-&amp;gt;&amp;gt;A: POST /invocations

 A-&amp;gt;&amp;gt;O: client_credentials grant
 O--&amp;gt;&amp;gt;A: JWT (iss, aud, scp: mcp:read mcp:write)

 A-&amp;gt;&amp;gt;AG: LLM request (no auth needed)
 AG-&amp;gt;&amp;gt;AG: PII redaction, injection guard
 AG-&amp;gt;&amp;gt;LLM: Forward (AG injects API key)
 LLM--&amp;gt;&amp;gt;AG: Response
 AG-&amp;gt;&amp;gt;AG: Credential leak check
 AG--&amp;gt;&amp;gt;A: Sanitized response

 A-&amp;gt;&amp;gt;AG: MCP tool call + Bearer JWT
 AG-&amp;gt;&amp;gt;AG: Validate JWT (JWKS, iss, aud, exp)
 AG-&amp;gt;&amp;gt;AG: Check scopes vs tool (RBAC)
 AG-&amp;gt;&amp;gt;AG: Check deny list (destructive ops)
 AG-&amp;gt;&amp;gt;MCP: Execute tool call
 MCP--&amp;gt;&amp;gt;AG: Result
 AG--&amp;gt;&amp;gt;A: Tool result

 A--&amp;gt;&amp;gt;R: Final response
 R--&amp;gt;&amp;gt;I: Response
&lt;/div>
&lt;h3 id="two-layers-of-access-control">Two Layers of Access Control&lt;/h3>
&lt;p>&lt;strong>Layer 1: AWS IAM&lt;/strong> — controls who can invoke the agent. Standard AWS access management. You already know this.&lt;/p>
&lt;p>&lt;strong>Layer 2: agentgateway (Okta JWT + RBAC)&lt;/strong> — controls what the agent can do with MCP tools. This is where it gets interesting.&lt;/p>
&lt;h3 id="okta-configuration">Okta Configuration&lt;/h3>
&lt;p>One OAuth2 service app provides the agent&amp;rsquo;s identity:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_app_oauth&amp;#34; &amp;#34;agentcore_service&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> label&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;devops-copilot-service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> grant_types&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;client_credentials&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> token_endpoint_auth_method&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;client_secret_basic&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> response_types&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Custom MCP scopes control fine-grained tool access:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_auth_server_scope&amp;#34; &amp;#34;mcp_read&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;mcp:read&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> consent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;IMPLICIT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_auth_server_scope&amp;#34; &amp;#34;mcp_write&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;mcp:write&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> consent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;IMPLICIT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_auth_server_scope&amp;#34; &amp;#34;mcp_admin&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;mcp:admin&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> consent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;REQUIRED&amp;#34;&lt;/span>&lt;span class="c1"> # Explicit consent required
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="how-the-agent-gets-its-token">How the Agent Gets Its Token&lt;/h3>
&lt;p>The agent uses &lt;code>client_credentials&lt;/code> — simple, no user interaction:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">get_okta_token&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Get Okta JWT via client_credentials. Cached with 5-min buffer.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;expires_at&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">300&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">http_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">OKTA_TOKEN_URL&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;grant_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;client_credentials&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;scope&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;mcp:read mcp:write&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">auth&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OKTA_CLIENT_ID&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">OKTA_CLIENT_SECRET&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;expires_at&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;expires_in&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3600&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The JWT payload:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;iss&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://integrator-7147223.okta.com/oauth2/aus104zseyg64swj3698&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;aud&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;api://default&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;sub&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0oa...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;mcp:read&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;mcp:write&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;exp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1739544600&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every MCP call includes this token:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">headers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Authorization&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="agentgateway-enforces-three-layers-of-mcp-access-control">agentgateway Enforces Three Layers of MCP Access Control&lt;/h3>
&lt;p>&lt;strong>Layer 1: JWT Authentication&lt;/strong> — Enterprise policy validates Okta JWTs on every MCP request. No valid token = no tool access.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">traffic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwtAuthentication&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Strict&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://your-okta.okta.com/oauth2/your-auth-server&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;api://default&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remote&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://your-okta.okta.com/oauth2/your-auth-server/v1/keys&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cacheDuration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3600s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Layer 2: Scope-Based RBAC&lt;/strong> — CEL expressions match JWT scopes to tool operations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Allow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchExpressions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> claims.scp.exists(s, s == &amp;#39;mcp:read&amp;#39;) &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tool.name.startsWith(&amp;#39;list_&amp;#39;) || tool.name.startsWith(&amp;#39;get_&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> claims.scp.exists(s, s == &amp;#39;mcp:write&amp;#39;) &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tool.name.startsWith(&amp;#39;post_&amp;#39;) || tool.name.startsWith(&amp;#39;create_&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scope&lt;/th>
&lt;th>Allowed&lt;/th>
&lt;th>Denied&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>mcp:read&lt;/code>&lt;/td>
&lt;td>List channels, get issues, search code&lt;/td>
&lt;td>Post messages, create issues&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp:write&lt;/code>&lt;/td>
&lt;td>All of read + post, create, comment&lt;/td>
&lt;td>Delete, admin operations&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp:admin&lt;/code>&lt;/td>
&lt;td>All of write + admin operations&lt;/td>
&lt;td>Destructive ops (always blocked)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Key detail: &lt;code>tools/list&lt;/code> responses are &lt;strong>filtered&lt;/strong> (unauthorized tools hidden from the agent), &lt;code>tools/call&lt;/code> requests &lt;strong>rejected&lt;/strong> if scopes don&amp;rsquo;t match.&lt;/p>
&lt;p>&lt;strong>Layer 3: Destructive Operation Blocking&lt;/strong> — always denied regardless of scope:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">denyPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchExpressions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tool.name.contains(&amp;#39;delete&amp;#39;) || tool.name.contains(&amp;#39;merge_pull_request&amp;#39;)&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="why-not-both-gateways">Why Not Both Gateways?&lt;/h2>
&lt;p>&amp;ldquo;Why not keep the AgentCore Gateway &lt;em>and&lt;/em> use agentgateway?&amp;rdquo;&lt;/p>
&lt;p>You can. But you shouldn&amp;rsquo;t. Here&amp;rsquo;s why:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Redundant JWT validation.&lt;/strong> Both validate the same Okta token. One is enough. Having two means debugging auth failures in two places.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>AgentCore Gateway can&amp;rsquo;t do what matters.&lt;/strong> It validates JWTs. Great. But it doesn&amp;rsquo;t do PII filtering, prompt injection detection, scope-based MCP RBAC, rate limiting per token budget, or LLM failover. You need agentgateway for all of that anyway.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Portability.&lt;/strong> The &lt;code>EnterpriseAgentgatewayPolicy&lt;/code> you write for AWS works identically on GKE, AKS, bare metal, or your laptop. The AgentCore Gateway works on AWS only.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Fewer moving parts.&lt;/strong> One gateway, one auth config, one place to look when something breaks.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The demo architecture is intentionally minimal:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AWS provides compute&lt;/strong> (AgentCore Runtime)&lt;/li>
&lt;li>&lt;strong>agentgateway provides governance&lt;/strong> (auth, RBAC, security, observability)&lt;/li>
&lt;li>&lt;strong>Okta provides identity&lt;/strong> (JWT tokens)&lt;/li>
&lt;/ul>
&lt;p>Each component does one thing well.&lt;/p>
&lt;hr>
&lt;h2 id="guardrails-the-full-picture">Guardrails: The Full Picture&lt;/h2>
&lt;h3 id="security-policies">Security Policies&lt;/h3>
&lt;p>&lt;strong>PII Protection.&lt;/strong> Before a prompt reaches the LLM, agentgateway scans for personally identifiable information and redacts it. Social security numbers, email addresses, phone numbers — stripped before they leave your network.&lt;/p>
&lt;p>&lt;strong>Prompt Injection Detection.&lt;/strong> Agents process user input. Users (or attackers) submit malicious prompts designed to hijack the agent&amp;rsquo;s behavior. agentgateway detects common jailbreak patterns and blocks them before the LLM ever sees them.&lt;/p>
&lt;p>&lt;strong>Credential Leak Prevention.&lt;/strong> LLMs sometimes echo back credentials that appeared in training data or context. agentgateway scans responses for patterns matching API keys, tokens, and passwords, blocking them before they reach users.&lt;/p>
&lt;h3 id="traffic-management">Traffic Management&lt;/h3>
&lt;p>&lt;strong>Rate limiting&lt;/strong> operates at two levels — requests and tokens:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rate_limiting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">requests_per_minute&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">tokens_per_minute&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Per-identity limits (from JWT &lt;code>sub&lt;/code> claim) prevent any single agent or user from monopolizing LLM capacity.&lt;/p>
&lt;p>&lt;strong>Multi-provider failover:&lt;/strong> Configure primary and fallback providers. Anthropic down? agentgateway routes to OpenAI automatically — your agent code doesn&amp;rsquo;t change.&lt;/p>
&lt;h3 id="observability">Observability&lt;/h3>
&lt;p>Every LLM call and MCP tool invocation is traced end-to-end:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Langfuse&lt;/strong> — LLM-specific traces with prompt/completion pairs, token usage, latency, cost&lt;/li>
&lt;li>&lt;strong>ClickHouse&lt;/strong> — raw OTLP spans for custom dashboards and alerting&lt;/li>
&lt;li>&lt;strong>Identity attribution&lt;/strong> — every trace tagged with the JWT identity that triggered it&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="the-takeaway">The Takeaway&lt;/h2>
&lt;p>Your cloud provider should run your agents. It shouldn&amp;rsquo;t govern them.&lt;/p>
&lt;p>agentgateway gives you one place to enforce auth, RBAC, security policies, rate limits, and observability — regardless of where your agents run. Write the policy once, deploy it anywhere.&lt;/p>
&lt;p>The &lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">aws-agentcore-demo&lt;/a> proves it: a fully functional agent on AWS Bedrock AgentCore, with zero vendor-specific gateways, and complete governance through agentgateway.&lt;/p>
&lt;p>&lt;strong>Resources:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.solo.io/products/agentgateway">Enterprise agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">Demo repo&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>You wouldn&amp;rsquo;t deploy microservices without an API gateway. So why are teams deploying AI agents with direct, ungoverned access to LLMs and external tools?&lt;/p>
&lt;p>If you&amp;rsquo;re running agents on AWS Bedrock AgentCore — or anywhere else — and those agents are calling Claude, GPT-4, or hitting MCP tool servers directly, you have a governance gap. No rate limiting. No audit trail. No PII filtering. No failover. Every team wiring up their own retry logic, their own auth handling, their own cost tracking. It&amp;rsquo;s 2016 microservices all over again, except the blast radius includes sending your customer database to a third-party LLM.&lt;/p>
&lt;p>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway&lt;/a> fixes this. It&amp;rsquo;s an open-source (CNCF) gateway purpose-built for AI agent traffic — both LLM calls and MCP tool calls — giving you a single control plane for everything your agents talk to.&lt;/p>
&lt;p>But here&amp;rsquo;s the thing: you don&amp;rsquo;t need &lt;em>another&lt;/em> gateway from your cloud provider to do it. agentgateway is your gateway — for auth, RBAC, observability, and security. Your cloud just provides compute.&lt;/p>
&lt;p>This post walks through why, how the auth chain works end-to-end, and how the &lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">aws-agentcore-demo&lt;/a> wires it all together.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem-agents-without-guardrails">The Problem: Agents Without Guardrails&lt;/h2>
&lt;p>An AI agent is, at its core, a loop: receive input → call an LLM → maybe call some tools → return output. The interesting part is what happens in those calls.&lt;/p>
&lt;p>When an agent on AgentCore calls Anthropic&amp;rsquo;s API directly:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>API keys live in the agent container.&lt;/strong> Every developer who can deploy an agent has access to production LLM credentials.&lt;/li>
&lt;li>&lt;strong>No visibility.&lt;/strong> What prompts are being sent? What data is in them? You don&amp;rsquo;t know until something leaks.&lt;/li>
&lt;li>&lt;strong>No cost controls.&lt;/strong> One runaway agent loop burns through your API budget in minutes. There&amp;rsquo;s no per-user or per-agent rate limiting.&lt;/li>
&lt;li>&lt;strong>No failover.&lt;/strong> Anthropic has an outage? Your agent is dead. Hope someone set up a retry with exponential backoff — oh wait, each team did it differently.&lt;/li>
&lt;li>&lt;strong>No security policies.&lt;/strong> PII goes straight to the LLM. Prompt injection attempts pass through unfiltered. Credentials in responses get forwarded to users.&lt;/li>
&lt;/ul>
&lt;p>And then there&amp;rsquo;s MCP. Your agent discovers tools — Slack, GitHub, internal APIs — via Model Context Protocol servers. Each MCP server needs auth. Each one is another surface to secure, another thing to monitor, another connection to manage.&lt;/p>
&lt;p>Every team solving these problems independently is wasted engineering. Worse, most teams don&amp;rsquo;t solve them at all.&lt;/p>
&lt;hr>
&lt;h2 id="the-trap-another-vendor-gateway">The Trap: Another Vendor Gateway&lt;/h2>
&lt;p>Cloud providers see this problem too. Their solution? Another managed service. AWS has AgentCore Gateway. Others will follow.&lt;/p>
&lt;p>But think about what that means:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You&amp;rsquo;re locked in.&lt;/strong> That gateway only works on AWS. Move to GCP, Azure, or on-prem? Rebuild your auth, RBAC, and observability from scratch.&lt;/li>
&lt;li>&lt;strong>Double the gateways.&lt;/strong> Now you have a cloud gateway &lt;em>and&lt;/em> your actual governance layer. Two hops, two configurations, two things to debug.&lt;/li>
&lt;li>&lt;strong>Weakest-link security.&lt;/strong> If your cloud gateway does JWT validation but your real tools don&amp;rsquo;t, you have a false sense of security. If both do it, you&amp;rsquo;re paying for redundancy.&lt;/li>
&lt;li>&lt;strong>Vendor roadmap dependency.&lt;/strong> Need scope-based MCP RBAC? PII filtering? Prompt injection detection? You&amp;rsquo;re waiting on their feature backlog.&lt;/li>
&lt;/ul>
&lt;p>The right answer: use your cloud provider for what it&amp;rsquo;s good at — &lt;strong>compute&lt;/strong> — and use a purpose-built, portable gateway for &lt;strong>governance&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="the-solution-agentgateway">The Solution: agentgateway&lt;/h2>
&lt;p>agentgateway sits between your agents and everything they talk to. LLMs and MCP tool servers both route through it.&lt;/p>
&lt;p>This isn&amp;rsquo;t Envoy with some AI plugins bolted on. agentgateway is a purpose-built proxy with its own xDS-inspired control plane, designed from the ground up for agent traffic patterns. It understands:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM protocols&lt;/strong> — OpenAI-compatible chat completions, streaming, token counting&lt;/li>
&lt;li>&lt;strong>MCP protocol&lt;/strong> — tool discovery, tool invocation, server lifecycle&lt;/li>
&lt;li>&lt;strong>Agent-specific policies&lt;/strong> — PII redaction, prompt injection detection, per-user token budgets&lt;/li>
&lt;li>&lt;strong>JWT authentication&lt;/strong> — validate Okta/Entra/any OIDC tokens on MCP routes with scope-based RBAC&lt;/li>
&lt;/ul>
&lt;p>Two logical functions, one binary:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>LLM Proxy&lt;/strong> — OpenAI-compatible endpoint that routes to any provider (Anthropic, OpenAI, xAI, local models)&lt;/li>
&lt;li>&lt;strong>MCP Gateway&lt;/strong> — aggregates multiple MCP servers, presents a unified tool catalog to agents, enforces auth + RBAC&lt;/li>
&lt;/ol>
&lt;p>Your agent code barely changes. Point it at agentgateway instead of the LLM provider directly. Add a Bearer token to MCP calls. That&amp;rsquo;s it.&lt;/p>
&lt;hr>
&lt;h2 id="traffic-flow-how-this-demo-works">Traffic Flow: How This Demo Works&lt;/h2>
&lt;p>The &lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">aws-agentcore-demo&lt;/a> is a working reference architecture. Here&amp;rsquo;s what&amp;rsquo;s actually running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌───────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ AWS Bedrock AgentCore │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌───────────────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Agent Container │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (arm64, Python/FastAPI) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Gets Okta JWT │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (client_credentials) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └──────┬──────────┬──────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌──────▼──┐ ┌────▼─────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Runtime │ │ (no gateway) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Endpoint │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └─────────┘ └──────────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└────────────────────┼────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ ngrok tunnels
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ▼
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ k8s-rooster (on-prem k8s) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ agentgateway (Enterprise) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ ┌──────────┐ ┌──────────────────┐ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ LLM │ │ MCP │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ Proxy │ │ Gateway │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ (no auth)│ │ (Okta JWT auth) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │ (scope RBAC) │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ └────┬─────┘ └──────┬───────────┘ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└───────┼──────────────┼─────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌────▼────┐ ┌──────▼──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Anthropic│ │ MCP Servers │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │OpenAI │ │ - Slack │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │xAI │ │ - GitHub │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────┘ │ - Tools │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>The agent container&lt;/strong> runs on AgentCore as an arm64 Python/FastAPI application. It doesn&amp;rsquo;t hold any LLM API keys. It knows two endpoints: agentgateway&amp;rsquo;s LLM proxy and MCP gateway.&lt;/p>
&lt;p>&lt;strong>LLM calls&lt;/strong> go through agentgateway&amp;rsquo;s OpenAI-compatible proxy — no auth needed. agentgateway holds the API keys (stored as Kubernetes Secrets, managed by ArgoCD), applies policies, logs the interaction, and returns the response.&lt;/p>
&lt;p>&lt;strong>MCP tool calls&lt;/strong> go through agentgateway&amp;rsquo;s MCP gateway. The agent includes its Okta JWT (obtained via &lt;code>client_credentials&lt;/code> grant) in every MCP request. agentgateway validates the token, checks scopes, and enforces RBAC before the tool call reaches the upstream MCP server.&lt;/p>
&lt;p>&lt;strong>Connectivity&lt;/strong> between AWS and the on-prem k8s-rooster cluster uses ngrok tunnels — pragmatic for a demo. In production, you&amp;rsquo;d use VPC peering, PrivateLink, or similar.&lt;/p>
&lt;p>&lt;strong>No AgentCore Gateway.&lt;/strong> The agent container is invoked directly through the AgentCore Runtime endpoint. AWS IAM controls who can invoke the agent. agentgateway controls what the agent can do.&lt;/p>
&lt;hr>
&lt;h2 id="authentication-end-to-end">Authentication: End-to-End&lt;/h2>
&lt;p>Every MCP request traverses an authenticated chain — from AWS IAM through Okta to tool invocation. &lt;strong>No ambient credentials, no hardcoded API keys in agent code, no unauthenticated MCP hops.&lt;/strong>&lt;/p>
&lt;h3 id="the-auth-stack">The Auth Stack&lt;/h3>
&lt;div class="mermaid">sequenceDiagram
 participant I as Invoker
 participant IAM as AWS IAM
 participant R as AgentCore Runtime
 participant A as Agent
 participant O as Okta
 participant AG as agentgateway
 participant LLM as LLM Provider
 participant MCP as MCP Tools

 I-&amp;gt;&amp;gt;IAM: invoke-agent-runtime (SigV4)
 IAM-&amp;gt;&amp;gt;R: Authorized → forward to agent
 R-&amp;gt;&amp;gt;A: POST /invocations

 A-&amp;gt;&amp;gt;O: client_credentials grant
 O--&amp;gt;&amp;gt;A: JWT (iss, aud, scp: mcp:read mcp:write)

 A-&amp;gt;&amp;gt;AG: LLM request (no auth needed)
 AG-&amp;gt;&amp;gt;AG: PII redaction, injection guard
 AG-&amp;gt;&amp;gt;LLM: Forward (AG injects API key)
 LLM--&amp;gt;&amp;gt;AG: Response
 AG-&amp;gt;&amp;gt;AG: Credential leak check
 AG--&amp;gt;&amp;gt;A: Sanitized response

 A-&amp;gt;&amp;gt;AG: MCP tool call + Bearer JWT
 AG-&amp;gt;&amp;gt;AG: Validate JWT (JWKS, iss, aud, exp)
 AG-&amp;gt;&amp;gt;AG: Check scopes vs tool (RBAC)
 AG-&amp;gt;&amp;gt;AG: Check deny list (destructive ops)
 AG-&amp;gt;&amp;gt;MCP: Execute tool call
 MCP--&amp;gt;&amp;gt;AG: Result
 AG--&amp;gt;&amp;gt;A: Tool result

 A--&amp;gt;&amp;gt;R: Final response
 R--&amp;gt;&amp;gt;I: Response
&lt;/div>
&lt;h3 id="two-layers-of-access-control">Two Layers of Access Control&lt;/h3>
&lt;p>&lt;strong>Layer 1: AWS IAM&lt;/strong> — controls who can invoke the agent. Standard AWS access management. You already know this.&lt;/p>
&lt;p>&lt;strong>Layer 2: agentgateway (Okta JWT + RBAC)&lt;/strong> — controls what the agent can do with MCP tools. This is where it gets interesting.&lt;/p>
&lt;h3 id="okta-configuration">Okta Configuration&lt;/h3>
&lt;p>One OAuth2 service app provides the agent&amp;rsquo;s identity:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_app_oauth&amp;#34; &amp;#34;agentcore_service&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> label&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;devops-copilot-service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;service&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> grant_types&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;client_credentials&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> token_endpoint_auth_method&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;client_secret_basic&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> response_types&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Custom MCP scopes control fine-grained tool access:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_auth_server_scope&amp;#34; &amp;#34;mcp_read&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;mcp:read&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> consent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;IMPLICIT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_auth_server_scope&amp;#34; &amp;#34;mcp_write&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;mcp:write&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> consent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;IMPLICIT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;okta_auth_server_scope&amp;#34; &amp;#34;mcp_admin&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;mcp:admin&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> consent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;REQUIRED&amp;#34;&lt;/span>&lt;span class="c1"> # Explicit consent required
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="how-the-agent-gets-its-token">How the Agent Gets Its Token&lt;/h3>
&lt;p>The agent uses &lt;code>client_credentials&lt;/code> — simple, no user interaction:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">get_okta_token&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Get Okta JWT via client_credentials. Cached with 5-min buffer.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;expires_at&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">-&lt;/span> &lt;span class="mi">300&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">http_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">post&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">OKTA_TOKEN_URL&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;grant_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;client_credentials&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;scope&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;mcp:read mcp:write&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">auth&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">OKTA_CLIENT_ID&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">OKTA_CLIENT_SECRET&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">resp&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">_token_cache&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;expires_at&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">time&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">time&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;expires_in&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3600&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;access_token&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The JWT payload:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;iss&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;https://integrator-7147223.okta.com/oauth2/aus104zseyg64swj3698&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;aud&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;api://default&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;sub&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0oa...&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;scp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;mcp:read&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;mcp:write&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;exp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1739544600&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every MCP call includes this token:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">headers&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;Authorization&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sa">f&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="si">{&lt;/span>&lt;span class="n">token&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="agentgateway-enforces-three-layers-of-mcp-access-control">agentgateway Enforces Three Layers of MCP Access Control&lt;/h3>
&lt;p>&lt;strong>Layer 1: JWT Authentication&lt;/strong> — Enterprise policy validates Okta JWTs on every MCP request. No valid token = no tool access.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">traffic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwtAuthentication&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Strict&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">providers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">issuer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://your-okta.okta.com/oauth2/your-auth-server&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">audiences&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;api://default&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">jwks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">remote&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;https://your-okta.okta.com/oauth2/your-auth-server/v1/keys&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cacheDuration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3600s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Layer 2: Scope-Based RBAC&lt;/strong> — CEL expressions match JWT scopes to tool operations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Allow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchExpressions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> claims.scp.exists(s, s == &amp;#39;mcp:read&amp;#39;) &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tool.name.startsWith(&amp;#39;list_&amp;#39;) || tool.name.startsWith(&amp;#39;get_&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> claims.scp.exists(s, s == &amp;#39;mcp:write&amp;#39;) &amp;amp;&amp;amp; (
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tool.name.startsWith(&amp;#39;post_&amp;#39;) || tool.name.startsWith(&amp;#39;create_&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> )&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Scope&lt;/th>
&lt;th>Allowed&lt;/th>
&lt;th>Denied&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>mcp:read&lt;/code>&lt;/td>
&lt;td>List channels, get issues, search code&lt;/td>
&lt;td>Post messages, create issues&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp:write&lt;/code>&lt;/td>
&lt;td>All of read + post, create, comment&lt;/td>
&lt;td>Delete, admin operations&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp:admin&lt;/code>&lt;/td>
&lt;td>All of write + admin operations&lt;/td>
&lt;td>Destructive ops (always blocked)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Key detail: &lt;code>tools/list&lt;/code> responses are &lt;strong>filtered&lt;/strong> (unauthorized tools hidden from the agent), &lt;code>tools/call&lt;/code> requests &lt;strong>rejected&lt;/strong> if scopes don&amp;rsquo;t match.&lt;/p>
&lt;p>&lt;strong>Layer 3: Destructive Operation Blocking&lt;/strong> — always denied regardless of scope:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">denyPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchExpressions&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">&amp;gt;-&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> tool.name.contains(&amp;#39;delete&amp;#39;) || tool.name.contains(&amp;#39;merge_pull_request&amp;#39;)&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="why-not-both-gateways">Why Not Both Gateways?&lt;/h2>
&lt;p>&amp;ldquo;Why not keep the AgentCore Gateway &lt;em>and&lt;/em> use agentgateway?&amp;rdquo;&lt;/p>
&lt;p>You can. But you shouldn&amp;rsquo;t. Here&amp;rsquo;s why:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Redundant JWT validation.&lt;/strong> Both validate the same Okta token. One is enough. Having two means debugging auth failures in two places.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>AgentCore Gateway can&amp;rsquo;t do what matters.&lt;/strong> It validates JWTs. Great. But it doesn&amp;rsquo;t do PII filtering, prompt injection detection, scope-based MCP RBAC, rate limiting per token budget, or LLM failover. You need agentgateway for all of that anyway.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Portability.&lt;/strong> The &lt;code>EnterpriseAgentgatewayPolicy&lt;/code> you write for AWS works identically on GKE, AKS, bare metal, or your laptop. The AgentCore Gateway works on AWS only.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Fewer moving parts.&lt;/strong> One gateway, one auth config, one place to look when something breaks.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The demo architecture is intentionally minimal:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AWS provides compute&lt;/strong> (AgentCore Runtime)&lt;/li>
&lt;li>&lt;strong>agentgateway provides governance&lt;/strong> (auth, RBAC, security, observability)&lt;/li>
&lt;li>&lt;strong>Okta provides identity&lt;/strong> (JWT tokens)&lt;/li>
&lt;/ul>
&lt;p>Each component does one thing well.&lt;/p>
&lt;hr>
&lt;h2 id="guardrails-the-full-picture">Guardrails: The Full Picture&lt;/h2>
&lt;h3 id="security-policies">Security Policies&lt;/h3>
&lt;p>&lt;strong>PII Protection.&lt;/strong> Before a prompt reaches the LLM, agentgateway scans for personally identifiable information and redacts it. Social security numbers, email addresses, phone numbers — stripped before they leave your network.&lt;/p>
&lt;p>&lt;strong>Prompt Injection Detection.&lt;/strong> Agents process user input. Users (or attackers) submit malicious prompts designed to hijack the agent&amp;rsquo;s behavior. agentgateway detects common jailbreak patterns and blocks them before the LLM ever sees them.&lt;/p>
&lt;p>&lt;strong>Credential Leak Prevention.&lt;/strong> LLMs sometimes echo back credentials that appeared in training data or context. agentgateway scans responses for patterns matching API keys, tokens, and passwords, blocking them before they reach users.&lt;/p>
&lt;h3 id="traffic-management">Traffic Management&lt;/h3>
&lt;p>&lt;strong>Rate limiting&lt;/strong> operates at two levels — requests and tokens:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rate_limiting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">match&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">requests_per_minute&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">tokens_per_minute&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Per-identity limits (from JWT &lt;code>sub&lt;/code> claim) prevent any single agent or user from monopolizing LLM capacity.&lt;/p>
&lt;p>&lt;strong>Multi-provider failover:&lt;/strong> Configure primary and fallback providers. Anthropic down? agentgateway routes to OpenAI automatically — your agent code doesn&amp;rsquo;t change.&lt;/p>
&lt;h3 id="observability">Observability&lt;/h3>
&lt;p>Every LLM call and MCP tool invocation is traced end-to-end:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Langfuse&lt;/strong> — LLM-specific traces with prompt/completion pairs, token usage, latency, cost&lt;/li>
&lt;li>&lt;strong>ClickHouse&lt;/strong> — raw OTLP spans for custom dashboards and alerting&lt;/li>
&lt;li>&lt;strong>Identity attribution&lt;/strong> — every trace tagged with the JWT identity that triggered it&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="the-takeaway">The Takeaway&lt;/h2>
&lt;p>Your cloud provider should run your agents. It shouldn&amp;rsquo;t govern them.&lt;/p>
&lt;p>agentgateway gives you one place to enforce auth, RBAC, security policies, rate limits, and observability — regardless of where your agents run. Write the policy once, deploy it anywhere.&lt;/p>
&lt;p>The &lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">aws-agentcore-demo&lt;/a> proves it: a fully functional agent on AWS Bedrock AgentCore, with zero vendor-specific gateways, and complete governance through agentgateway.&lt;/p>
&lt;p>&lt;strong>Resources:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/agentgateway/agentgateway">agentgateway GitHub&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.solo.io/products/agentgateway">Enterprise agentgateway&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/aws-agentcore-demo">Demo repo&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Connect Any IDE to GitHub MCP Server Through agentgateway</title><link>https://maniak.io/articles/2026-02-19-cursor-github-mcp-through-agentgateway/</link><pubDate>Thu, 19 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-19-cursor-github-mcp-through-agentgateway/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Modern AI-powered IDEs and coding agents ship with built-in MCP client support. That means you can connect them to GitHub&amp;rsquo;s remote MCP server to give your AI assistant direct access to issues, pull requests, code search, and repository management.&lt;/p>
&lt;p>But connecting directly to GitHub means no visibility into what tools are being called, no rate limiting, and no centralized credential management. Every developer has their own PAT, every tool call goes straight to GitHub ungoverned.&lt;/p>
&lt;p>This guide shows you how to route MCP traffic from &lt;strong>any IDE&lt;/strong> — Cursor, VS Code, Windsurf, Claude Code, or OpenCode — to &lt;strong>GitHub&amp;rsquo;s remote MCP server&lt;/strong> through &lt;strong>&lt;a href="https://agentgateway.dev">Solo agentgateway&lt;/a>&lt;/strong>. You deploy the gateway once, and every IDE on your team connects through it.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>One gateway, every IDE&lt;/strong>: Deploy agentgateway once, connect all your tools&lt;/li>
&lt;li>&lt;strong>Centralized credentials&lt;/strong>: GitHub PAT lives in a Kubernetes secret, not on every developer laptop&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong>: Control tool call volume per IDE or per user&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong>: OpenTelemetry traces for every MCP interaction&lt;/li>
&lt;li>&lt;strong>No self-hosted MCP server&lt;/strong>: GitHub hosts the MCP server at &lt;code>api.githubcopilot.com&lt;/code> — you just proxy&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ VS Code │──┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Cursor │──┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ ┌──────────────────┐ ┌──────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Windsurf │──┼──MCP──▶│ agentgateway │──MCP──▶│ api.githubcopilot.com │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ │ (K8s proxy) │ │ (GitHub Remote MCP) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code │──┤ └──────────────────┘ └──────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ │ Auth
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ OpenCode │──┘ │ Rate limiting
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ │ OTLP traces
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind&lt;/a> installed&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code> and &lt;code>helm&lt;/code> installed&lt;/li>
&lt;li>A &lt;a href="https://github.com/settings/tokens">GitHub Personal Access Token&lt;/a> (PAT)&lt;/li>
&lt;li>At least one of: Cursor, VS Code (with Copilot), Windsurf, Claude Code, or OpenCode&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-agentgateway">Step 2: Install agentgateway&lt;/h2>
&lt;p>Deploy the Kubernetes Gateway API CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the agentgateway CRDs and control plane:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway-crds oci://ghcr.io/kgateway-dev/charts/agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify the control plane is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-create-an-agentgateway-proxy">Step 3: Create an agentgateway Proxy&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for the proxy pod to be ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4-configure-the-github-remote-mcp-backend">Step 4: Configure the GitHub Remote MCP Backend&lt;/h2>
&lt;p>GitHub hosts a remote MCP server at &lt;code>https://api.githubcopilot.com/mcp/&lt;/code>. We point agentgateway at it instead of deploying our own.&lt;/p>
&lt;p>Create a secret with your GitHub PAT:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic github-mcp-secret &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer ghp_your_token_here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create the backend:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: github-mcp-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targets:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-target
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> static:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> host: api.githubcopilot.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 443
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /mcp/
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: github-mcp-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tls:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sni: api.githubcopilot.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key details:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>path: /mcp/&lt;/code>&lt;/strong> — GitHub&amp;rsquo;s MCP endpoint&lt;/li>
&lt;li>&lt;strong>&lt;code>tls.sni&lt;/code>&lt;/strong> — required for HTTPS on port 443&lt;/li>
&lt;li>&lt;strong>&lt;code>auth.secretRef&lt;/code>&lt;/strong> — injects the &lt;code>Authorization: Bearer &amp;lt;PAT&amp;gt;&lt;/code> header automatically&lt;/li>
&lt;/ul>
&lt;h2 id="step-5-create-the-httproute">Step 5: Create the HTTPRoute&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: github-mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /github
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: github-mcp-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-port-forward">Step 6: Port-Forward&lt;/h2>
&lt;p>Expose the gateway locally:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system deployment/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Leave this running. All IDE configs below use &lt;code>http://localhost:8080&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="ide-configuration">IDE Configuration&lt;/h2>
&lt;p>Each IDE has its own MCP config format. Pick yours below — or configure all of them to use the same gateway endpoint.&lt;/p>
&lt;h3 id="cursor">Cursor&lt;/h3>
&lt;p>Create or edit &lt;code>~/.cursor/mcp.json&lt;/code> (global) or &lt;code>.cursor/mcp.json&lt;/code> (per-project):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Cursor → &lt;strong>Settings → MCP&lt;/strong> → verify GitHub tools appear.&lt;/p>
&lt;h3 id="vs-code-with-github-copilot">VS Code (with GitHub Copilot)&lt;/h3>
&lt;p>Add to your VS Code &lt;code>settings.json&lt;/code> (Cmd/Ctrl + , → search &amp;ldquo;mcp&amp;rdquo;):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github.copilot.chat.mcp.servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or for workspace-specific config, add to &lt;code>.vscode/settings.json&lt;/code>.&lt;/p>
&lt;p>Reload VS Code (Cmd/Ctrl + Shift + P → &amp;ldquo;Developer: Reload Window&amp;rdquo;) → open Copilot Chat → tools should list GitHub MCP tools.&lt;/p>
&lt;h3 id="windsurf">Windsurf&lt;/h3>
&lt;p>Create or edit &lt;code>~/.windsurf/mcp.json&lt;/code> (global) or &lt;code>.windsurf/mcp.json&lt;/code> (per-project):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Windsurf → verify connection in MCP settings.&lt;/p>
&lt;h3 id="claude-code">Claude Code&lt;/h3>
&lt;p>Add via CLI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">claude mcp add github --transport sse http://localhost:8080/github/mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or add to your project&amp;rsquo;s &lt;code>.mcp.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="opencode">OpenCode&lt;/h3>
&lt;p>Add to your &lt;code>opencode.json&lt;/code> config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-7-test-it">Step 7: Test It&lt;/h2>
&lt;p>Open your IDE&amp;rsquo;s AI chat and try:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;List open issues in my repo&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Create a new branch called feature/test&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Search for files referencing agentgateway&amp;rdquo;&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Every tool call flows through agentgateway regardless of which IDE you&amp;rsquo;re using. The gateway handles auth, logs the interaction, and forwards to GitHub.&lt;/p>
&lt;h2 id="verifying-traffic">Verifying Traffic&lt;/h2>
&lt;p>Check proxy logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system deploy/agentgateway-proxy --tail&lt;span class="o">=&lt;/span>&lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see MCP connection events and tool call forwards to &lt;code>api.githubcopilot.com&lt;/code>.&lt;/p>
&lt;h2 id="ide-comparison">IDE Comparison&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>IDE&lt;/th>
&lt;th>Config File&lt;/th>
&lt;th>Transport&lt;/th>
&lt;th>Auth Headers&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Cursor&lt;/strong>&lt;/td>
&lt;td>&lt;code>~/.cursor/mcp.json&lt;/code>&lt;/td>
&lt;td>streamable-http&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>VS Code&lt;/strong>&lt;/td>
&lt;td>&lt;code>settings.json&lt;/code>&lt;/td>
&lt;td>streamable-http&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Windsurf&lt;/strong>&lt;/td>
&lt;td>&lt;code>~/.windsurf/mcp.json&lt;/code>&lt;/td>
&lt;td>streamable-http&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Claude Code&lt;/strong>&lt;/td>
&lt;td>&lt;code>.mcp.json&lt;/code> or CLI&lt;/td>
&lt;td>SSE&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenCode&lt;/strong>&lt;/td>
&lt;td>&lt;code>opencode.json&lt;/code>&lt;/td>
&lt;td>SSE&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Note&lt;/strong>: All IDEs connect to the same &lt;code>http://localhost:8080/github/mcp&lt;/code> endpoint. The gateway doesn&amp;rsquo;t care which client is calling — it applies the same auth, rate limiting, and observability to all of them.&lt;/p>
&lt;/blockquote>
&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Add more MCP servers&lt;/strong>: Slack, Notion, Jira — all behind the same gateway, same endpoint pattern&lt;/li>
&lt;li>&lt;strong>Security policies&lt;/strong>: JWT auth, prompt guards, tool-level RBAC with &lt;code>AgentgatewayPolicy&lt;/code>&lt;/li>
&lt;li>&lt;strong>Team rollout&lt;/strong>: Share the gateway endpoint with your team — one PAT, centrally managed, no credentials on laptops&lt;/li>
&lt;li>&lt;strong>Per-IDE rate limiting&lt;/strong>: Different limits for different tools or users&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl delete httproute github-mcp -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend github-mcp-backend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret github-mcp-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Every AI-powered IDE now speaks MCP. But connecting each one directly to backend servers means fragmented credentials, zero visibility, and no governance. agentgateway gives you a single control point — deploy it once, connect every IDE on your team, and get auth, rate limiting, and observability on every tool call.&lt;/p>
&lt;p>One gateway. Every IDE. Full visibility.&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Modern AI-powered IDEs and coding agents ship with built-in MCP client support. That means you can connect them to GitHub&amp;rsquo;s remote MCP server to give your AI assistant direct access to issues, pull requests, code search, and repository management.&lt;/p>
&lt;p>But connecting directly to GitHub means no visibility into what tools are being called, no rate limiting, and no centralized credential management. Every developer has their own PAT, every tool call goes straight to GitHub ungoverned.&lt;/p>
&lt;p>This guide shows you how to route MCP traffic from &lt;strong>any IDE&lt;/strong> — Cursor, VS Code, Windsurf, Claude Code, or OpenCode — to &lt;strong>GitHub&amp;rsquo;s remote MCP server&lt;/strong> through &lt;strong>&lt;a href="https://agentgateway.dev">Solo agentgateway&lt;/a>&lt;/strong>. You deploy the gateway once, and every IDE on your team connects through it.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>One gateway, every IDE&lt;/strong>: Deploy agentgateway once, connect all your tools&lt;/li>
&lt;li>&lt;strong>Centralized credentials&lt;/strong>: GitHub PAT lives in a Kubernetes secret, not on every developer laptop&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong>: Control tool call volume per IDE or per user&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong>: OpenTelemetry traces for every MCP interaction&lt;/li>
&lt;li>&lt;strong>No self-hosted MCP server&lt;/strong>: GitHub hosts the MCP server at &lt;code>api.githubcopilot.com&lt;/code> — you just proxy&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ VS Code │──┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Cursor │──┤
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ ┌──────────────────┐ ┌──────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Windsurf │──┼──MCP──▶│ agentgateway │──MCP──▶│ api.githubcopilot.com │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ │ (K8s proxy) │ │ (GitHub Remote MCP) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code │──┤ └──────────────────┘ └──────────────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├──────────────┤ │ │ Auth
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ OpenCode │──┘ │ Rate limiting
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ │ OTLP traces
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind&lt;/a> installed&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code> and &lt;code>helm&lt;/code> installed&lt;/li>
&lt;li>A &lt;a href="https://github.com/settings/tokens">GitHub Personal Access Token&lt;/a> (PAT)&lt;/li>
&lt;li>At least one of: Cursor, VS Code (with Copilot), Windsurf, Claude Code, or OpenCode&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-agentgateway">Step 2: Install agentgateway&lt;/h2>
&lt;p>Deploy the Kubernetes Gateway API CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the agentgateway CRDs and control plane:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway-crds oci://ghcr.io/kgateway-dev/charts/agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> agentgateway oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify the control plane is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-create-an-agentgateway-proxy">Step 3: Create an agentgateway Proxy&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for the proxy pod to be ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl rollout status deploy/agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4-configure-the-github-remote-mcp-backend">Step 4: Configure the GitHub Remote MCP Backend&lt;/h2>
&lt;p>GitHub hosts a remote MCP server at &lt;code>https://api.githubcopilot.com/mcp/&lt;/code>. We point agentgateway at it instead of deploying our own.&lt;/p>
&lt;p>Create a secret with your GitHub PAT:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic github-mcp-secret &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer ghp_your_token_here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create the backend:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: github-mcp-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targets:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mcp-target
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> static:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> host: api.githubcopilot.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 443
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /mcp/
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: github-mcp-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tls:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sni: api.githubcopilot.com
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Key details:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>path: /mcp/&lt;/code>&lt;/strong> — GitHub&amp;rsquo;s MCP endpoint&lt;/li>
&lt;li>&lt;strong>&lt;code>tls.sni&lt;/code>&lt;/strong> — required for HTTPS on port 443&lt;/li>
&lt;li>&lt;strong>&lt;code>auth.secretRef&lt;/code>&lt;/strong> — injects the &lt;code>Authorization: Bearer &amp;lt;PAT&amp;gt;&lt;/code> header automatically&lt;/li>
&lt;/ul>
&lt;h2 id="step-5-create-the-httproute">Step 5: Create the HTTPRoute&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: github-mcp
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /github
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: github-mcp-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-port-forward">Step 6: Port-Forward&lt;/h2>
&lt;p>Expose the gateway locally:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system deployment/agentgateway-proxy 8080:80
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Leave this running. All IDE configs below use &lt;code>http://localhost:8080&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="ide-configuration">IDE Configuration&lt;/h2>
&lt;p>Each IDE has its own MCP config format. Pick yours below — or configure all of them to use the same gateway endpoint.&lt;/p>
&lt;h3 id="cursor">Cursor&lt;/h3>
&lt;p>Create or edit &lt;code>~/.cursor/mcp.json&lt;/code> (global) or &lt;code>.cursor/mcp.json&lt;/code> (per-project):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Cursor → &lt;strong>Settings → MCP&lt;/strong> → verify GitHub tools appear.&lt;/p>
&lt;h3 id="vs-code-with-github-copilot">VS Code (with GitHub Copilot)&lt;/h3>
&lt;p>Add to your VS Code &lt;code>settings.json&lt;/code> (Cmd/Ctrl + , → search &amp;ldquo;mcp&amp;rdquo;):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github.copilot.chat.mcp.servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or for workspace-specific config, add to &lt;code>.vscode/settings.json&lt;/code>.&lt;/p>
&lt;p>Reload VS Code (Cmd/Ctrl + Shift + P → &amp;ldquo;Developer: Reload Window&amp;rdquo;) → open Copilot Chat → tools should list GitHub MCP tools.&lt;/p>
&lt;h3 id="windsurf">Windsurf&lt;/h3>
&lt;p>Create or edit &lt;code>~/.windsurf/mcp.json&lt;/code> (global) or &lt;code>.windsurf/mcp.json&lt;/code> (per-project):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Windsurf → verify connection in MCP settings.&lt;/p>
&lt;h3 id="claude-code">Claude Code&lt;/h3>
&lt;p>Add via CLI:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">claude mcp add github --transport sse http://localhost:8080/github/mcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or add to your project&amp;rsquo;s &lt;code>.mcp.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="opencode">OpenCode&lt;/h3>
&lt;p>Add to your &lt;code>opencode.json&lt;/code> config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;servers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;github&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;sse&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;http://localhost:8080/github/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-7-test-it">Step 7: Test It&lt;/h2>
&lt;p>Open your IDE&amp;rsquo;s AI chat and try:&lt;/p>
&lt;ul>
&lt;li>&lt;em>&amp;ldquo;List open issues in my repo&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Create a new branch called feature/test&amp;rdquo;&lt;/em>&lt;/li>
&lt;li>&lt;em>&amp;ldquo;Search for files referencing agentgateway&amp;rdquo;&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Every tool call flows through agentgateway regardless of which IDE you&amp;rsquo;re using. The gateway handles auth, logs the interaction, and forwards to GitHub.&lt;/p>
&lt;h2 id="verifying-traffic">Verifying Traffic&lt;/h2>
&lt;p>Check proxy logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system deploy/agentgateway-proxy --tail&lt;span class="o">=&lt;/span>&lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see MCP connection events and tool call forwards to &lt;code>api.githubcopilot.com&lt;/code>.&lt;/p>
&lt;h2 id="ide-comparison">IDE Comparison&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>IDE&lt;/th>
&lt;th>Config File&lt;/th>
&lt;th>Transport&lt;/th>
&lt;th>Auth Headers&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Cursor&lt;/strong>&lt;/td>
&lt;td>&lt;code>~/.cursor/mcp.json&lt;/code>&lt;/td>
&lt;td>streamable-http&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>VS Code&lt;/strong>&lt;/td>
&lt;td>&lt;code>settings.json&lt;/code>&lt;/td>
&lt;td>streamable-http&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Windsurf&lt;/strong>&lt;/td>
&lt;td>&lt;code>~/.windsurf/mcp.json&lt;/code>&lt;/td>
&lt;td>streamable-http&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Claude Code&lt;/strong>&lt;/td>
&lt;td>&lt;code>.mcp.json&lt;/code> or CLI&lt;/td>
&lt;td>SSE&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenCode&lt;/strong>&lt;/td>
&lt;td>&lt;code>opencode.json&lt;/code>&lt;/td>
&lt;td>SSE&lt;/td>
&lt;td>✅ supported&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>Note&lt;/strong>: All IDEs connect to the same &lt;code>http://localhost:8080/github/mcp&lt;/code> endpoint. The gateway doesn&amp;rsquo;t care which client is calling — it applies the same auth, rate limiting, and observability to all of them.&lt;/p>
&lt;/blockquote>
&lt;h2 id="whats-next">What&amp;rsquo;s Next&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Add more MCP servers&lt;/strong>: Slack, Notion, Jira — all behind the same gateway, same endpoint pattern&lt;/li>
&lt;li>&lt;strong>Security policies&lt;/strong>: JWT auth, prompt guards, tool-level RBAC with &lt;code>AgentgatewayPolicy&lt;/code>&lt;/li>
&lt;li>&lt;strong>Team rollout&lt;/strong>: Share the gateway endpoint with your team — one PAT, centrally managed, no credentials on laptops&lt;/li>
&lt;li>&lt;strong>Per-IDE rate limiting&lt;/strong>: Different limits for different tools or users&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl delete httproute github-mcp -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend github-mcp-backend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret github-mcp-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Every AI-powered IDE now speaks MCP. But connecting each one directly to backend servers means fragmented credentials, zero visibility, and no governance. agentgateway gives you a single control point — deploy it once, connect every IDE on your team, and get auth, rate limiting, and observability on every tool call.&lt;/p>
&lt;p>One gateway. Every IDE. Full visibility.&lt;/p></content:encoded></item><item><title>Open Source LLM Observability: Tracing AI Calls with agentgateway and Langfuse</title><link>https://maniak.io/articles/2026-02-18-llm-observability-agentgateway-langfuse/</link><pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-18-llm-observability-agentgateway-langfuse/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>You&amp;rsquo;ve got your AI gateway routing LLM traffic. But can you actually &lt;em>see&lt;/em> what&amp;rsquo;s happening? Which models are being called, how many tokens are being consumed, what prompts are going in and what completions are coming back?&lt;/p>
&lt;p>This guide shows you how to integrate &lt;a href="https://langfuse.com">Langfuse&lt;/a> with &lt;a href="https://agentgateway.dev">Solo agentgateway&lt;/a> to get full observability over every LLM request — without touching your application code. We&amp;rsquo;ll go from zero to traced requests on a local kind cluster in under 10 minutes.&lt;/p>
&lt;h2 id="why-this-matters">Why This Matters&lt;/h2>
&lt;p>agentgateway already captures rich telemetry about every LLM request: model, tokens, latency, route, and security policy actions. By forwarding those traces to Langfuse, you get:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Full prompt and completion visibility&lt;/strong> across all LLM providers (OpenAI, Anthropic, xAI, etc.)&lt;/li>
&lt;li>&lt;strong>Token usage and cost tracking&lt;/strong> per model, route, and user&lt;/li>
&lt;li>&lt;strong>Latency analysis&lt;/strong> with gateway-level metadata — which route, which backend, which policy fired&lt;/li>
&lt;li>&lt;strong>Zero application changes&lt;/strong> — tracing happens at the gateway layer, not in your app&lt;/li>
&lt;/ul>
&lt;p>This is the power of observability at the infrastructure layer. Your developers don&amp;rsquo;t need to instrument anything. Every LLM call that flows through the gateway is automatically captured.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s what we&amp;rsquo;re building:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐ ┌────────────────────────┐ ┌─────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Your App / │ │ Solo agentgateway │ │ LLM Provider │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ AI Agent │────▶│ (Gateway API) │────▶│ (OpenAI, etc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ └───────────┬────────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP Traces (gRPC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌───────────▼────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenTelemetry │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Collector │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────┬───────────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP HTTP OTLP gRPC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌─────▼──┐ ┌─────▼──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Langfuse│ │Other backends │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ UI │ │(Jaeger, etc.) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway natively emits OpenTelemetry traces for every LLM request. A lightweight OTel Collector receives those traces and forwards them to Langfuse via OTLP HTTP. The collector can also fan-out to additional backends like Jaeger, Datadog, or ClickHouse if you need traces in multiple places.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind&lt;/a> installed&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/docs/tasks/tools/">kubectl&lt;/a> installed&lt;/li>
&lt;li>&lt;a href="https://helm.sh/docs/intro/install/">helm&lt;/a> installed&lt;/li>
&lt;li>A &lt;a href="https://cloud.langfuse.com">Langfuse&lt;/a> account (free tier works) or self-hosted instance&lt;/li>
&lt;li>An OpenAI API key (or any supported LLM provider)&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cluster-info --context kind-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-the-gateway-api-crds">Step 2: Install the Gateway API CRDs&lt;/h2>
&lt;p>agentgateway uses the standard Kubernetes Gateway API for configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-install-agentgateway">Step 3: Install agentgateway&lt;/h2>
&lt;p>Install the CRDs and control plane:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway-crds oci://cr.agentgateway.dev/helm/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify everything is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gatewayclass agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4-get-your-langfuse-credentials">Step 4: Get Your Langfuse Credentials&lt;/h2>
&lt;p>Log in to Langfuse and go to &lt;strong>Settings → API Keys&lt;/strong>. Create a new key pair (or use an existing one). Base64 encode them for the collector config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;pk-lf-YOUR_PUBLIC_KEY:sk-lf-YOUR_SECRET_KEY&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> base64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-5-deploy-the-otel-collector">Step 5: Deploy the OTel Collector&lt;/h2>
&lt;p>The collector bridges agentgateway (OTLP gRPC) and Langfuse (OTLP HTTP). Create &lt;code>langfuse-collector.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config.yaml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4318
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlphttp/langfuse:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: https://cloud.langfuse.com/api/public/otel
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Authorization: &amp;#34;Basic &amp;lt;YOUR_BASE64_CREDENTIALS&amp;gt;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> retry_on_failure:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> initial_interval: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_interval: 30s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_elapsed_time: 300s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> batch:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> send_batch_size: 1000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> timeout: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> pipelines:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> traces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers: [otlp]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors: [batch]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters: [otlphttp/langfuse]&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib:0.132.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--config=/conf/config.yaml&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Deploy it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f langfuse-collector.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-enable-tracing-on-agentgateway">Step 6: Enable Tracing on agentgateway&lt;/h2>
&lt;p>For agentgateway OSS, configure tracing via Helm values. Create &lt;code>values-tracing.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">gateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Upgrade the installation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values-tracing.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Using agentgateway Enterprise?&lt;/strong> Instead of environment variables, create an &lt;code>EnterpriseAgentgatewayParameters&lt;/code> resource with full control over field mappings:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.operation.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;chat&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.provider&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.requestModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.response.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.responseModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.prompt_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.inputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.completion_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.outputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.total_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.totalTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.temperature&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.params.temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.prompt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.completion&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-7-create-a-gateway-and-llm-route">Step 7: Create a Gateway and LLM Route&lt;/h2>
&lt;p>Create the Gateway resource and an OpenAI route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Same&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># openai-route.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create the secret and apply everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-api-key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f openai-route.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-8-test-it">Step 8: Test It&lt;/h2>
&lt;p>Port-forward the gateway and send a request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/ai-gateway 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:8080/openai/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4.1-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from agentgateway!&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-9-view-traces-in-langfuse">Step 9: View Traces in Langfuse&lt;/h2>
&lt;p>Open your Langfuse UI and navigate to &lt;strong>Traces&lt;/strong>. You should see a new trace with the full picture:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Model and provider&lt;/strong> — which model actually served the request&lt;/li>
&lt;li>&lt;strong>Token counts&lt;/strong> — input, output, and total tokens consumed&lt;/li>
&lt;li>&lt;strong>Full prompt and completion&lt;/strong> — the exact content sent and received&lt;/li>
&lt;li>&lt;strong>Gateway metadata&lt;/strong> — route name, backend endpoint, listener&lt;/li>
&lt;/ul>
&lt;h2 id="what-gets-captured">What Gets Captured&lt;/h2>
&lt;p>agentgateway follows the &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI semantic conventions&lt;/a>, so every trace includes structured attributes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>What It Tells You&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gen_ai.system&lt;/code>&lt;/td>
&lt;td>LLM provider (openai, anthropic, etc.)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>&lt;/td>
&lt;td>The model you asked for&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.model&lt;/code>&lt;/td>
&lt;td>The model that actually responded&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.prompt_tokens&lt;/code>&lt;/td>
&lt;td>Input tokens consumed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.completion_tokens&lt;/code>&lt;/td>
&lt;td>Output tokens generated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt&lt;/code>&lt;/td>
&lt;td>Full prompt content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.completion&lt;/code>&lt;/td>
&lt;td>Full completion content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gateway&lt;/code>&lt;/td>
&lt;td>agentgateway resource name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>route&lt;/code>&lt;/td>
&lt;td>HTTPRoute that matched&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>endpoint&lt;/code>&lt;/td>
&lt;td>Backend LLM endpoint&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="going-further">Going Further&lt;/h2>
&lt;h3 id="fan-out-to-multiple-backends">Fan-Out to Multiple Backends&lt;/h3>
&lt;p>The OTel Collector makes it easy to send traces to Langfuse &lt;em>and&lt;/em> another backend simultaneously:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Basic &amp;lt;CREDENTIALS&amp;gt;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp/jaeger&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jaeger-collector:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse, otlp/jaeger]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="mcp-tool-tracing">MCP Tool Tracing&lt;/h3>
&lt;p>agentgateway also traces MCP (Model Context Protocol) traffic. When an agent discovers or invokes tools through an MCP server proxied by agentgateway, you&amp;rsquo;ll see tool discovery requests, execution calls with parameters and results, and backend server latency — all as spans within the same trace.&lt;/p>
&lt;h3 id="security-policy-visibility">Security Policy Visibility&lt;/h3>
&lt;p>When agentgateway&amp;rsquo;s security policies fire — PII protection, prompt injection detection, credential leak prevention — the trace metadata shows which policy triggered, what action was taken (block, mask, allow), and what pattern matched. You get observability into both your LLM interactions and your security guardrails in one place.&lt;/p>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>No traces appearing?&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Check the collector is running: &lt;code>kubectl get pods -n agentgateway-system -l app=langfuse-otel-collector&lt;/code>&lt;/li>
&lt;li>Check collector logs for errors: &lt;code>kubectl logs -n agentgateway-system -l app=langfuse-otel-collector&lt;/code>&lt;/li>
&lt;li>Wrong API keys show up as 401 errors in collector logs&lt;/li>
&lt;li>Make sure proxies were restarted after configuring tracing&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Traces visible in gateway logs but not Langfuse?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Langfuse only supports OTLP HTTP, not gRPC — that&amp;rsquo;s why the collector is needed for protocol conversion&lt;/li>
&lt;li>Verify the exporter endpoint URL includes &lt;code>/api/public/otel&lt;/code>&lt;/li>
&lt;li>Double-check the Base64 credentials format: &lt;code>base64(public_key:secret_key)&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Incomplete trace data?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>For Enterprise, ensure &lt;code>fields.add&lt;/code> includes the GenAI attribute mappings&lt;/li>
&lt;li>Set &lt;code>randomSampling: true&lt;/code> to capture all requests during testing&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/">Solo agentgateway Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway OSS&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs">Langfuse Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs/integrations/opentelemetry">Langfuse OpenTelemetry Integration&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI Conventions&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse">Source Code for This Guide&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>You&amp;rsquo;ve got your AI gateway routing LLM traffic. But can you actually &lt;em>see&lt;/em> what&amp;rsquo;s happening? Which models are being called, how many tokens are being consumed, what prompts are going in and what completions are coming back?&lt;/p>
&lt;p>This guide shows you how to integrate &lt;a href="https://langfuse.com">Langfuse&lt;/a> with &lt;a href="https://agentgateway.dev">Solo agentgateway&lt;/a> to get full observability over every LLM request — without touching your application code. We&amp;rsquo;ll go from zero to traced requests on a local kind cluster in under 10 minutes.&lt;/p>
&lt;h2 id="why-this-matters">Why This Matters&lt;/h2>
&lt;p>agentgateway already captures rich telemetry about every LLM request: model, tokens, latency, route, and security policy actions. By forwarding those traces to Langfuse, you get:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Full prompt and completion visibility&lt;/strong> across all LLM providers (OpenAI, Anthropic, xAI, etc.)&lt;/li>
&lt;li>&lt;strong>Token usage and cost tracking&lt;/strong> per model, route, and user&lt;/li>
&lt;li>&lt;strong>Latency analysis&lt;/strong> with gateway-level metadata — which route, which backend, which policy fired&lt;/li>
&lt;li>&lt;strong>Zero application changes&lt;/strong> — tracing happens at the gateway layer, not in your app&lt;/li>
&lt;/ul>
&lt;p>This is the power of observability at the infrastructure layer. Your developers don&amp;rsquo;t need to instrument anything. Every LLM call that flows through the gateway is automatically captured.&lt;/p>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;p>Here&amp;rsquo;s what we&amp;rsquo;re building:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐ ┌────────────────────────┐ ┌─────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Your App / │ │ Solo agentgateway │ │ LLM Provider │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ AI Agent │────▶│ (Gateway API) │────▶│ (OpenAI, etc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ └───────────┬────────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP Traces (gRPC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌───────────▼────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenTelemetry │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Collector │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────┬───────────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP HTTP OTLP gRPC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌─────▼──┐ ┌─────▼──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Langfuse│ │Other backends │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ UI │ │(Jaeger, etc.) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway natively emits OpenTelemetry traces for every LLM request. A lightweight OTel Collector receives those traces and forwards them to Langfuse via OTLP HTTP. The collector can also fan-out to additional backends like Jaeger, Datadog, or ClickHouse if you need traces in multiple places.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/get-docker/">Docker&lt;/a> installed and running&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">kind&lt;/a> installed&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/docs/tasks/tools/">kubectl&lt;/a> installed&lt;/li>
&lt;li>&lt;a href="https://helm.sh/docs/intro/install/">helm&lt;/a> installed&lt;/li>
&lt;li>A &lt;a href="https://cloud.langfuse.com">Langfuse&lt;/a> account (free tier works) or self-hosted instance&lt;/li>
&lt;li>An OpenAI API key (or any supported LLM provider)&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind Cluster&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cluster-info --context kind-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-the-gateway-api-crds">Step 2: Install the Gateway API CRDs&lt;/h2>
&lt;p>agentgateway uses the standard Kubernetes Gateway API for configuration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-3-install-agentgateway">Step 3: Install agentgateway&lt;/h2>
&lt;p>Install the CRDs and control plane:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway-crds oci://cr.agentgateway.dev/helm/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify everything is running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gatewayclass agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-4-get-your-langfuse-credentials">Step 4: Get Your Langfuse Credentials&lt;/h2>
&lt;p>Log in to Langfuse and go to &lt;strong>Settings → API Keys&lt;/strong>. Create a new key pair (or use an existing one). Base64 encode them for the collector config:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;pk-lf-YOUR_PUBLIC_KEY:sk-lf-YOUR_SECRET_KEY&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> base64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-5-deploy-the-otel-collector">Step 5: Deploy the OTel Collector&lt;/h2>
&lt;p>The collector bridges agentgateway (OTLP gRPC) and Langfuse (OTLP HTTP). Create &lt;code>langfuse-collector.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config.yaml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4318
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlphttp/langfuse:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: https://cloud.langfuse.com/api/public/otel
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Authorization: &amp;#34;Basic &amp;lt;YOUR_BASE64_CREDENTIALS&amp;gt;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> retry_on_failure:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> initial_interval: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_interval: 30s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_elapsed_time: 300s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> batch:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> send_batch_size: 1000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> timeout: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> pipelines:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> traces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers: [otlp]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors: [batch]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters: [otlphttp/langfuse]&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib:0.132.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--config=/conf/config.yaml&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Deploy it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f langfuse-collector.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-enable-tracing-on-agentgateway">Step 6: Enable Tracing on agentgateway&lt;/h2>
&lt;p>For agentgateway OSS, configure tracing via Helm values. Create &lt;code>values-tracing.yaml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">gateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Upgrade the installation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values-tracing.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Using agentgateway Enterprise?&lt;/strong> Instead of environment variables, create an &lt;code>EnterpriseAgentgatewayParameters&lt;/code> resource with full control over field mappings:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.operation.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;chat&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.provider&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.requestModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.response.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.responseModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.prompt_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.inputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.completion_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.outputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.total_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.totalTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.temperature&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.params.temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.prompt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.completion&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-7-create-a-gateway-and-llm-route">Step 7: Create a Gateway and LLM Route&lt;/h2>
&lt;p>Create the Gateway resource and an OpenAI route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Same&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># openai-route.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create the secret and apply everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-api-key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f openai-route.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-8-test-it">Step 8: Test It&lt;/h2>
&lt;p>Port-forward the gateway and send a request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/ai-gateway 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:8080/openai/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4.1-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from agentgateway!&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-9-view-traces-in-langfuse">Step 9: View Traces in Langfuse&lt;/h2>
&lt;p>Open your Langfuse UI and navigate to &lt;strong>Traces&lt;/strong>. You should see a new trace with the full picture:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Model and provider&lt;/strong> — which model actually served the request&lt;/li>
&lt;li>&lt;strong>Token counts&lt;/strong> — input, output, and total tokens consumed&lt;/li>
&lt;li>&lt;strong>Full prompt and completion&lt;/strong> — the exact content sent and received&lt;/li>
&lt;li>&lt;strong>Gateway metadata&lt;/strong> — route name, backend endpoint, listener&lt;/li>
&lt;/ul>
&lt;h2 id="what-gets-captured">What Gets Captured&lt;/h2>
&lt;p>agentgateway follows the &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI semantic conventions&lt;/a>, so every trace includes structured attributes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>What It Tells You&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gen_ai.system&lt;/code>&lt;/td>
&lt;td>LLM provider (openai, anthropic, etc.)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>&lt;/td>
&lt;td>The model you asked for&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.model&lt;/code>&lt;/td>
&lt;td>The model that actually responded&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.prompt_tokens&lt;/code>&lt;/td>
&lt;td>Input tokens consumed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.completion_tokens&lt;/code>&lt;/td>
&lt;td>Output tokens generated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt&lt;/code>&lt;/td>
&lt;td>Full prompt content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.completion&lt;/code>&lt;/td>
&lt;td>Full completion content&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gateway&lt;/code>&lt;/td>
&lt;td>agentgateway resource name&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>route&lt;/code>&lt;/td>
&lt;td>HTTPRoute that matched&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>endpoint&lt;/code>&lt;/td>
&lt;td>Backend LLM endpoint&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="going-further">Going Further&lt;/h2>
&lt;h3 id="fan-out-to-multiple-backends">Fan-Out to Multiple Backends&lt;/h3>
&lt;p>The OTel Collector makes it easy to send traces to Langfuse &lt;em>and&lt;/em> another backend simultaneously:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Basic &amp;lt;CREDENTIALS&amp;gt;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp/jaeger&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jaeger-collector:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse, otlp/jaeger]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="mcp-tool-tracing">MCP Tool Tracing&lt;/h3>
&lt;p>agentgateway also traces MCP (Model Context Protocol) traffic. When an agent discovers or invokes tools through an MCP server proxied by agentgateway, you&amp;rsquo;ll see tool discovery requests, execution calls with parameters and results, and backend server latency — all as spans within the same trace.&lt;/p>
&lt;h3 id="security-policy-visibility">Security Policy Visibility&lt;/h3>
&lt;p>When agentgateway&amp;rsquo;s security policies fire — PII protection, prompt injection detection, credential leak prevention — the trace metadata shows which policy triggered, what action was taken (block, mask, allow), and what pattern matched. You get observability into both your LLM interactions and your security guardrails in one place.&lt;/p>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>No traces appearing?&lt;/strong>&lt;/p>
&lt;ol>
&lt;li>Check the collector is running: &lt;code>kubectl get pods -n agentgateway-system -l app=langfuse-otel-collector&lt;/code>&lt;/li>
&lt;li>Check collector logs for errors: &lt;code>kubectl logs -n agentgateway-system -l app=langfuse-otel-collector&lt;/code>&lt;/li>
&lt;li>Wrong API keys show up as 401 errors in collector logs&lt;/li>
&lt;li>Make sure proxies were restarted after configuring tracing&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Traces visible in gateway logs but not Langfuse?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Langfuse only supports OTLP HTTP, not gRPC — that&amp;rsquo;s why the collector is needed for protocol conversion&lt;/li>
&lt;li>Verify the exporter endpoint URL includes &lt;code>/api/public/otel&lt;/code>&lt;/li>
&lt;li>Double-check the Base64 credentials format: &lt;code>base64(public_key:secret_key)&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Incomplete trace data?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>For Enterprise, ensure &lt;code>fields.add&lt;/code> includes the GenAI attribute mappings&lt;/li>
&lt;li>Set &lt;code>randomSampling: true&lt;/code> to capture all requests during testing&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/">Solo agentgateway Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway OSS&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs">Langfuse Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs/integrations/opentelemetry">Langfuse OpenTelemetry Integration&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI Conventions&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse">Source Code for This Guide&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Route MCP and LLM Traffic from Claude Desktop and Claude Code Through agentgateway</title><link>https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/</link><pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-18-route-mcp-traffic-claude-through-agentgateway/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Claude Desktop and Claude Code are powerful AI tools, but out of the box they talk directly to backend services with no visibility, no security controls, and no usage governance. Every MCP tool call from Claude Desktop hits your servers unmonitored. Every LLM API call from Claude Code goes straight to the provider with no rate limiting or audit trail.&lt;/p>
&lt;p>What if you could put a gateway in front of all that traffic?&lt;/p>
&lt;p>This guide shows you how to route both &lt;strong>MCP server traffic from Claude Desktop&lt;/strong> and &lt;strong>LLM API calls from Claude Code&lt;/strong> through &lt;a href="https://agentgateway.dev">Solo agentgateway&lt;/a> — giving you JWT authentication, observability traces, rate limiting, and centralized API key management. We&amp;rsquo;ll use &lt;strong>Anthropic&lt;/strong> as the LLM provider.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;p>By routing traffic through agentgateway, you gain:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Security&lt;/strong>: JWT authentication on MCP endpoints, RBAC for tool access, prompt injection guards&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong>: OpenTelemetry traces for every MCP tool call and LLM request&lt;/li>
&lt;li>&lt;strong>Rate Limiting&lt;/strong>: Token-based and request-based limits per user or team&lt;/li>
&lt;li>&lt;strong>Centralized Secrets&lt;/strong>: API keys live in Kubernetes secrets, not on developer laptops&lt;/li>
&lt;li>&lt;strong>Audit Trail&lt;/strong>: Full logging of every interaction for compliance&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────────┐ ┌──────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Desktop │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (MCP traffic) │─────────▶│ │──▶ MCP Servers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │ (math, github, etc.)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────┘ │ Solo agentgateway │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ (Gateway API) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌──────────────────┐ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code │ │ • JWT Auth │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (LLM traffic) │─────────▶│ • Rate Limiting │──▶ Anthropic API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ • OTel Tracing │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────┘ │ • Prompt Guards │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Claude Desktop connects to MCP servers &lt;em>through&lt;/em> the gateway. Claude Code sends its LLM API calls &lt;em>through&lt;/em> the gateway to Anthropic. Both streams get the same security and observability treatment.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Kubernetes cluster with agentgateway deployed (&lt;a href="https://docs.solo.io/agentgateway/latest/quickstart/">quickstart&lt;/a>)&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code> and &lt;code>helm&lt;/code> installed&lt;/li>
&lt;li>Anthropic API key&lt;/li>
&lt;li>Claude Desktop installed (for MCP routing)&lt;/li>
&lt;li>Claude Code CLI installed (for LLM routing): &lt;code>npm install -g @anthropic-ai/claude-code&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="part-1-deploy-an-mcp-server">Part 1: Deploy an MCP Server&lt;/h2>
&lt;p>We&amp;rsquo;ll deploy a simple math MCP server that Claude Desktop can call through the gateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f - &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: ConfigMap
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-script
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">data:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> server.py: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> import uvicorn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from mcp.server.fastmcp import FastMCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.applications import Starlette
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.routing import Route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.requests import Request
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.responses import JSONResponse, Response
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp = FastMCP(&amp;#34;Math-Service&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> @mcp.tool()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> def add(a: int, b: int) -&amp;gt; int:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return a + b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> @mcp.tool()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> def multiply(a: int, b: int) -&amp;gt; int:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return a * b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> async def handle_mcp(request: Request):
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> try:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> data = await request.json()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> method = data.get(&amp;#34;method&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> msg_id = data.get(&amp;#34;id&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = None
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if method == &amp;#34;initialize&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;protocolVersion&amp;#34;: &amp;#34;2024-11-05&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;capabilities&amp;#34;: {&amp;#34;tools&amp;#34;: {}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;serverInfo&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;Math-Service&amp;#34;, &amp;#34;version&amp;#34;: &amp;#34;1.0&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;notifications/initialized&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return Response(status_code=202)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;tools/list&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tools_list = await mcp.list_tools()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;tools&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;name&amp;#34;: t.name, &amp;#34;description&amp;#34;: t.description, &amp;#34;inputSchema&amp;#34;: t.inputSchema}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for t in tools_list
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;tools/call&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> params = data.get(&amp;#34;params&amp;#34;, {})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name = params.get(&amp;#34;name&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args = params.get(&amp;#34;arguments&amp;#34;, {})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tool_result = await mcp.call_tool(name, args)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> serialized = []
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for content in tool_result:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if hasattr(content, &amp;#34;type&amp;#34;) and content.type == &amp;#34;text&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> serialized.append({&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;, &amp;#34;text&amp;#34;: content.text})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> serialized.append(content if isinstance(content, dict) else str(content))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {&amp;#34;content&amp;#34;: serialized, &amp;#34;isError&amp;#34;: False}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;ping&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return JSONResponse(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: msg_id, &amp;#34;error&amp;#34;: {&amp;#34;code&amp;#34;: -32601, &amp;#34;message&amp;#34;: &amp;#34;Method not found&amp;#34;}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> status_code=404
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return JSONResponse({&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: msg_id, &amp;#34;result&amp;#34;: result})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> except Exception as e:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> import traceback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> traceback.print_exc()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return JSONResponse(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: None, &amp;#34;error&amp;#34;: {&amp;#34;code&amp;#34;: -32603, &amp;#34;message&amp;#34;: str(e)}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> status_code=500
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app = Starlette(routes=[
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> Route(&amp;#34;/mcp&amp;#34;, handle_mcp, methods=[&amp;#34;POST&amp;#34;]),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> Route(&amp;#34;/&amp;#34;, lambda r: JSONResponse({&amp;#34;status&amp;#34;: &amp;#34;ok&amp;#34;}), methods=[&amp;#34;GET&amp;#34;])
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ])
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if __name__ == &amp;#34;__main__&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> uvicorn.run(app, host=&amp;#34;0.0.0.0&amp;#34;, port=8000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: math
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: python:3.11-slim
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> command: [&amp;#34;/bin/sh&amp;#34;, &amp;#34;-c&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> pip install &amp;#34;mcp[cli]&amp;#34; uvicorn starlette &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> python /app/server.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumeMounts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: script-volume
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mountPath: /app
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> readinessProbe:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> httpGet:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> initialDelaySeconds: 5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> periodSeconds: 5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: script-volume
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> configMap:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-script
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for the pod to be ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>ready pod -l &lt;span class="nv">app&lt;/span>&lt;span class="o">=&lt;/span>mcp-math-server --timeout&lt;span class="o">=&lt;/span>120s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-2-create-the-gateway-and-routes">Part 2: Create the Gateway and Routes&lt;/h2>
&lt;p>We need two things routed through agentgateway: MCP tool traffic and LLM API traffic to Anthropic.&lt;/p>
&lt;h3 id="create-the-anthropic-api-key-secret">Create the Anthropic API Key Secret&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic anthropic-api-key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="nv">$ANTHROPIC_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-gateway">Create the Gateway&lt;/h3>
&lt;p>A single Gateway with one listener handles both MCP and LLM traffic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-mcp-backend-and-route">Create the MCP Backend and Route&lt;/h3>
&lt;p>Point the gateway at our math MCP server:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># mcp-backend.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">math-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">math-service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mcp-math-server.default.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StreamableHTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mcp-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">math-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-anthropic-llm-backend-and-route">Create the Anthropic LLM Backend and Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># anthropic-backend.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apply everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f mcp-backend.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f anthropic-backend.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="get-the-gateway-address">Get the Gateway Address&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl get svc ai-gateway -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -o &lt;span class="nv">jsonpath&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{.status.loadBalancer.ingress[0].ip}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Gateway: http://&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For local clusters (kind/minikube), use port-forward instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/ai-gateway 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>localhost
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-3-configure-claude-desktop-mcp-traffic">Part 3: Configure Claude Desktop (MCP Traffic)&lt;/h2>
&lt;p>Claude Desktop can route its MCP tool calls through agentgateway using &lt;a href="https://github.com/nichochar/supergateway">supergateway&lt;/a> as a local bridge.&lt;/p>
&lt;h3 id="config-file-location">Config File Location&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>macOS&lt;/strong>: &lt;code>~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code>&lt;/li>
&lt;li>&lt;strong>Windows&lt;/strong>: &lt;code>%APPDATA%\Claude\claude_desktop_config.json&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="basic-configuration">Basic Configuration&lt;/h3>
&lt;p>Create or update the config file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;math-service&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;npx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;-y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;supergateway&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--streamableHttp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;http://GATEWAY_IP:8080/mcp&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace &lt;code>GATEWAY_IP&lt;/code> with your actual gateway IP or &lt;code>localhost&lt;/code> if using port-forward.&lt;/p>
&lt;h3 id="with-jwt-authentication">With JWT Authentication&lt;/h3>
&lt;p>If you&amp;rsquo;ve configured JWT auth on the gateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;math-service&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;npx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;-y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;mcp-remote&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;http://GATEWAY_IP:8080/mcp&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;MCP_HEADERS&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Authorization: Bearer &amp;lt;your-jwt-token&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Claude Desktop after saving the config. You should see the math tools available in the tools menu.&lt;/p>
&lt;h2 id="part-4-configure-claude-code-llm-traffic-via-anthropic">Part 4: Configure Claude Code (LLM Traffic via Anthropic)&lt;/h2>
&lt;p>Claude Code can route its LLM API traffic through agentgateway to Anthropic. This means your API keys stay in Kubernetes secrets — not on developer machines.&lt;/p>
&lt;h3 id="set-the-base-url">Set the Base URL&lt;/h3>
&lt;p>Point Claude Code at the gateway&amp;rsquo;s Anthropic route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_BASE_URL&lt;/span>&lt;span class="o">=&lt;/span>http://&lt;span class="nv">$GATEWAY_IP&lt;/span>:8080/anthropic
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For persistence, add it to your shell profile:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;export ANTHROPIC_BASE_URL=http://&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/anthropic&amp;#34;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="run-claude-code">Run Claude Code&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">claude
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>All LLM traffic now flows through agentgateway to Anthropic. You can verify by checking the gateway logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system -l gateway.networking.k8s.io/gateway-name&lt;span class="o">=&lt;/span>ai-gateway -f
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-5-add-security-policies">Part 5: Add Security Policies&lt;/h2>
&lt;p>Now that traffic flows through the gateway, you can layer on security controls.&lt;/p>
&lt;h3 id="rate-limiting">Rate Limiting&lt;/h3>
&lt;p>Prevent runaway costs with token-based rate limiting:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rate-limit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimiting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenBucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refillRate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="prompt-guards">Prompt Guards&lt;/h3>
&lt;p>Block prompt injection attempts before they reach Anthropic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prompt-guard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">REJECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ignore (previous|all) instructions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apply the policies:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f rate-limit.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f prompt-guard.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-6-enable-observability">Part 6: Enable Observability&lt;/h2>
&lt;p>Add OpenTelemetry tracing to see every request in detail. If you followed our &lt;a href="2026-02-18-llm-observability-agentgateway-langfuse">Langfuse observability guide&lt;/a>, you already have this set up. Otherwise, configure tracing via Helm values:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values-tracing.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://your-otel-collector:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values-tracing.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With tracing enabled, every Claude Desktop tool call and every Claude Code LLM request shows up as a trace with full metadata: model, tokens, latency, route, and any security policy actions.&lt;/p>
&lt;h2 id="testing-the-full-setup">Testing the Full Setup&lt;/h2>
&lt;h3 id="test-mcp-claude-desktop">Test MCP (Claude Desktop)&lt;/h3>
&lt;p>Open Claude Desktop and ask it to use the math tools:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;What&amp;rsquo;s 42 multiplied by 17?&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Claude should invoke the &lt;code>multiply&lt;/code> tool through agentgateway. Check the gateway logs to confirm the request was proxied.&lt;/p>
&lt;h3 id="test-llm-claude-code">Test LLM (Claude Code)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ANTHROPIC_BASE_URL&lt;/span>&lt;span class="o">=&lt;/span>http://&lt;span class="nv">$GATEWAY_IP&lt;/span>:8080/anthropic claude
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># In Claude Code, type any prompt — it routes through the gateway to Anthropic&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-with-mcp-inspector">Test with MCP Inspector&lt;/h3>
&lt;p>You can also verify MCP connectivity directly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx @modelcontextprotocol/inspector
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Enter the URL: &lt;code>http://GATEWAY_IP:8080/mcp&lt;/code>&lt;/p>
&lt;p>You should see the math tools listed and be able to invoke them.&lt;/p>
&lt;h2 id="important-limitations">Important Limitations&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Claude Desktop&amp;rsquo;s core LLM traffic&lt;/strong> (conversations with Claude itself) cannot be routed through agentgateway — only MCP server traffic is proxied&lt;/li>
&lt;li>&lt;strong>Claude Code LLM traffic&lt;/strong> &lt;em>can&lt;/em> be fully routed through the gateway via the &lt;code>ANTHROPIC_BASE_URL&lt;/code> environment variable&lt;/li>
&lt;li>For local development (kind/minikube), you&amp;rsquo;ll need port-forwarding since there&amp;rsquo;s no external load balancer&lt;/li>
&lt;/ul>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>MCP tools not showing in Claude Desktop?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Restart Claude Desktop after config changes&lt;/li>
&lt;li>Verify the gateway is reachable: &lt;code>curl http://GATEWAY_IP:8080/mcp&lt;/code>&lt;/li>
&lt;li>Check that supergateway/mcp-remote is installed: &lt;code>npx -y supergateway --help&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Claude Code connection refused?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Verify the gateway service has an external IP: &lt;code>kubectl get svc -n agentgateway-system&lt;/code>&lt;/li>
&lt;li>Check firewall rules allow traffic on port 8080&lt;/li>
&lt;li>Ensure &lt;code>ANTHROPIC_BASE_URL&lt;/code> is set correctly: &lt;code>echo $ANTHROPIC_BASE_URL&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Timeout errors?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>LLM requests can take time — ensure gateway timeouts are configured for AI workloads&lt;/li>
&lt;li>Check gateway pod resources are sufficient for the traffic volume&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Authentication errors?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Verify the Anthropic API key secret exists: &lt;code>kubectl get secret anthropic-api-key -n agentgateway-system&lt;/code>&lt;/li>
&lt;li>Check the key is valid with a direct curl to Anthropic&amp;rsquo;s API&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl delete -f gateway.yaml -f mcp-backend.yaml -f anthropic-backend.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete deployment mcp-math-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete svc mcp-math-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete configmap mcp-math-script
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret anthropic-api-key -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/">Solo agentgateway Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway OSS&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anthropic.com/en/docs/claude-desktop/mcp">Claude Desktop MCP Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code CLI&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/AdminTurnedDevOps/agentic-demo-repo/tree/main/agent-desktop-configs">Original demo configs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anthropic.com/en/api">Anthropic API&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Claude Desktop and Claude Code are powerful AI tools, but out of the box they talk directly to backend services with no visibility, no security controls, and no usage governance. Every MCP tool call from Claude Desktop hits your servers unmonitored. Every LLM API call from Claude Code goes straight to the provider with no rate limiting or audit trail.&lt;/p>
&lt;p>What if you could put a gateway in front of all that traffic?&lt;/p>
&lt;p>This guide shows you how to route both &lt;strong>MCP server traffic from Claude Desktop&lt;/strong> and &lt;strong>LLM API calls from Claude Code&lt;/strong> through &lt;a href="https://agentgateway.dev">Solo agentgateway&lt;/a> — giving you JWT authentication, observability traces, rate limiting, and centralized API key management. We&amp;rsquo;ll use &lt;strong>Anthropic&lt;/strong> as the LLM provider.&lt;/p>
&lt;h2 id="what-you-get">What You Get&lt;/h2>
&lt;p>By routing traffic through agentgateway, you gain:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Security&lt;/strong>: JWT authentication on MCP endpoints, RBAC for tool access, prompt injection guards&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong>: OpenTelemetry traces for every MCP tool call and LLM request&lt;/li>
&lt;li>&lt;strong>Rate Limiting&lt;/strong>: Token-based and request-based limits per user or team&lt;/li>
&lt;li>&lt;strong>Centralized Secrets&lt;/strong>: API keys live in Kubernetes secrets, not on developer laptops&lt;/li>
&lt;li>&lt;strong>Audit Trail&lt;/strong>: Full logging of every interaction for compliance&lt;/li>
&lt;/ul>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────────┐ ┌──────────────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Desktop │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (MCP traffic) │─────────▶│ │──▶ MCP Servers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │ (math, github, etc.)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────┘ │ Solo agentgateway │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ (Gateway API) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">┌──────────────────┐ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Claude Code │ │ • JWT Auth │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ (LLM traffic) │─────────▶│ • Rate Limiting │──▶ Anthropic API
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ • OTel Tracing │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────────┘ │ • Prompt Guards │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └──────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Claude Desktop connects to MCP servers &lt;em>through&lt;/em> the gateway. Claude Code sends its LLM API calls &lt;em>through&lt;/em> the gateway to Anthropic. Both streams get the same security and observability treatment.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Kubernetes cluster with agentgateway deployed (&lt;a href="https://docs.solo.io/agentgateway/latest/quickstart/">quickstart&lt;/a>)&lt;/li>
&lt;li>&lt;code>kubectl&lt;/code> and &lt;code>helm&lt;/code> installed&lt;/li>
&lt;li>Anthropic API key&lt;/li>
&lt;li>Claude Desktop installed (for MCP routing)&lt;/li>
&lt;li>Claude Code CLI installed (for LLM routing): &lt;code>npm install -g @anthropic-ai/claude-code&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="part-1-deploy-an-mcp-server">Part 1: Deploy an MCP Server&lt;/h2>
&lt;p>We&amp;rsquo;ll deploy a simple math MCP server that Claude Desktop can call through the gateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f - &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: ConfigMap
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-script
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">data:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> server.py: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> import uvicorn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from mcp.server.fastmcp import FastMCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.applications import Starlette
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.routing import Route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.requests import Request
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from starlette.responses import JSONResponse, Response
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mcp = FastMCP(&amp;#34;Math-Service&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> @mcp.tool()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> def add(a: int, b: int) -&amp;gt; int:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return a + b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> @mcp.tool()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> def multiply(a: int, b: int) -&amp;gt; int:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return a * b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> async def handle_mcp(request: Request):
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> try:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> data = await request.json()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> method = data.get(&amp;#34;method&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> msg_id = data.get(&amp;#34;id&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = None
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if method == &amp;#34;initialize&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;protocolVersion&amp;#34;: &amp;#34;2024-11-05&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;capabilities&amp;#34;: {&amp;#34;tools&amp;#34;: {}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;serverInfo&amp;#34;: {&amp;#34;name&amp;#34;: &amp;#34;Math-Service&amp;#34;, &amp;#34;version&amp;#34;: &amp;#34;1.0&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;notifications/initialized&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return Response(status_code=202)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;tools/list&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tools_list = await mcp.list_tools()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;tools&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;name&amp;#34;: t.name, &amp;#34;description&amp;#34;: t.description, &amp;#34;inputSchema&amp;#34;: t.inputSchema}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for t in tools_list
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;tools/call&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> params = data.get(&amp;#34;params&amp;#34;, {})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name = params.get(&amp;#34;name&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args = params.get(&amp;#34;arguments&amp;#34;, {})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tool_result = await mcp.call_tool(name, args)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> serialized = []
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for content in tool_result:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if hasattr(content, &amp;#34;type&amp;#34;) and content.type == &amp;#34;text&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> serialized.append({&amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;, &amp;#34;text&amp;#34;: content.text})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> serialized.append(content if isinstance(content, dict) else str(content))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {&amp;#34;content&amp;#34;: serialized, &amp;#34;isError&amp;#34;: False}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> elif method == &amp;#34;ping&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> result = {}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return JSONResponse(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: msg_id, &amp;#34;error&amp;#34;: {&amp;#34;code&amp;#34;: -32601, &amp;#34;message&amp;#34;: &amp;#34;Method not found&amp;#34;}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> status_code=404
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return JSONResponse({&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: msg_id, &amp;#34;result&amp;#34;: result})
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> except Exception as e:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> import traceback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> traceback.print_exc()
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> return JSONResponse(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: None, &amp;#34;error&amp;#34;: {&amp;#34;code&amp;#34;: -32603, &amp;#34;message&amp;#34;: str(e)}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> status_code=500
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> )
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app = Starlette(routes=[
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> Route(&amp;#34;/mcp&amp;#34;, handle_mcp, methods=[&amp;#34;POST&amp;#34;]),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> Route(&amp;#34;/&amp;#34;, lambda r: JSONResponse({&amp;#34;status&amp;#34;: &amp;#34;ok&amp;#34;}), methods=[&amp;#34;GET&amp;#34;])
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ])
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if __name__ == &amp;#34;__main__&amp;#34;:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> uvicorn.run(app, host=&amp;#34;0.0.0.0&amp;#34;, port=8000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: math
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: python:3.11-slim
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> command: [&amp;#34;/bin/sh&amp;#34;, &amp;#34;-c&amp;#34;]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> pip install &amp;#34;mcp[cli]&amp;#34; uvicorn starlette &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> python /app/server.py
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumeMounts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: script-volume
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mountPath: /app
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> readinessProbe:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> httpGet:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> initialDelaySeconds: 5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> periodSeconds: 5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: script-volume
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> configMap:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-script
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mcp-math-server
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Wait for the pod to be ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>ready pod -l &lt;span class="nv">app&lt;/span>&lt;span class="o">=&lt;/span>mcp-math-server --timeout&lt;span class="o">=&lt;/span>120s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-2-create-the-gateway-and-routes">Part 2: Create the Gateway and Routes&lt;/h2>
&lt;p>We need two things routed through agentgateway: MCP tool traffic and LLM API traffic to Anthropic.&lt;/p>
&lt;h3 id="create-the-anthropic-api-key-secret">Create the Anthropic API Key Secret&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create secret generic anthropic-api-key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="nv">$ANTHROPIC_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-gateway">Create the Gateway&lt;/h3>
&lt;p>A single Gateway with one listener handles both MCP and LLM traffic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># gateway.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-mcp-backend-and-route">Create the MCP Backend and Route&lt;/h3>
&lt;p>Point the gateway at our math MCP server:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># mcp-backend.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">math-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mcp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">math-service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mcp-math-server.default.svc.cluster.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">StreamableHTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mcp-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">math-mcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-anthropic-llm-backend-and-route">Create the Anthropic LLM Backend and Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># anthropic-backend.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apply everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f mcp-backend.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f anthropic-backend.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="get-the-gateway-address">Get the Gateway Address&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl get svc ai-gateway -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -o &lt;span class="nv">jsonpath&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{.status.loadBalancer.ingress[0].ip}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Gateway: http://&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For local clusters (kind/minikube), use port-forward instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/ai-gateway 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>localhost
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-3-configure-claude-desktop-mcp-traffic">Part 3: Configure Claude Desktop (MCP Traffic)&lt;/h2>
&lt;p>Claude Desktop can route its MCP tool calls through agentgateway using &lt;a href="https://github.com/nichochar/supergateway">supergateway&lt;/a> as a local bridge.&lt;/p>
&lt;h3 id="config-file-location">Config File Location&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>macOS&lt;/strong>: &lt;code>~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code>&lt;/li>
&lt;li>&lt;strong>Windows&lt;/strong>: &lt;code>%APPDATA%\Claude\claude_desktop_config.json&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="basic-configuration">Basic Configuration&lt;/h3>
&lt;p>Create or update the config file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;math-service&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;npx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;-y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;supergateway&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--streamableHttp&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;http://GATEWAY_IP:8080/mcp&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace &lt;code>GATEWAY_IP&lt;/code> with your actual gateway IP or &lt;code>localhost&lt;/code> if using port-forward.&lt;/p>
&lt;h3 id="with-jwt-authentication">With JWT Authentication&lt;/h3>
&lt;p>If you&amp;rsquo;ve configured JWT auth on the gateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;mcpServers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;math-service&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;command&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;npx&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;args&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;-y&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;mcp-remote&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;http://GATEWAY_IP:8080/mcp&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;env&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;MCP_HEADERS&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Authorization: Bearer &amp;lt;your-jwt-token&amp;gt;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Claude Desktop after saving the config. You should see the math tools available in the tools menu.&lt;/p>
&lt;h2 id="part-4-configure-claude-code-llm-traffic-via-anthropic">Part 4: Configure Claude Code (LLM Traffic via Anthropic)&lt;/h2>
&lt;p>Claude Code can route its LLM API traffic through agentgateway to Anthropic. This means your API keys stay in Kubernetes secrets — not on developer machines.&lt;/p>
&lt;h3 id="set-the-base-url">Set the Base URL&lt;/h3>
&lt;p>Point Claude Code at the gateway&amp;rsquo;s Anthropic route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_BASE_URL&lt;/span>&lt;span class="o">=&lt;/span>http://&lt;span class="nv">$GATEWAY_IP&lt;/span>:8080/anthropic
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For persistence, add it to your shell profile:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;export ANTHROPIC_BASE_URL=http://&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/anthropic&amp;#34;&lt;/span> &amp;gt;&amp;gt; ~/.zshrc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="run-claude-code">Run Claude Code&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">claude
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>All LLM traffic now flows through agentgateway to Anthropic. You can verify by checking the gateway logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl logs -n agentgateway-system -l gateway.networking.k8s.io/gateway-name&lt;span class="o">=&lt;/span>ai-gateway -f
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-5-add-security-policies">Part 5: Add Security Policies&lt;/h2>
&lt;p>Now that traffic flows through the gateway, you can layer on security controls.&lt;/p>
&lt;h3 id="rate-limiting">Rate Limiting&lt;/h3>
&lt;p>Prevent runaway costs with token-based rate limiting:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rate-limit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rateLimiting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tokenBucket&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">maxTokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refillRate&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">refillInterval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">60s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="prompt-guards">Prompt Guards&lt;/h3>
&lt;p>Block prompt injection attempts before they reach Anthropic:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayPolicy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">prompt-guard&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">default&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">promptGuard&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">request&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">REJECT&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ignore (previous|all) instructions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Apply the policies:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f rate-limit.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f prompt-guard.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="part-6-enable-observability">Part 6: Enable Observability&lt;/h2>
&lt;p>Add OpenTelemetry tracing to see every request in detail. If you followed our &lt;a href="2026-02-18-llm-observability-agentgateway-langfuse">Langfuse observability guide&lt;/a>, you already have this set up. Otherwise, configure tracing via Helm values:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values-tracing.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://your-otel-collector:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values-tracing.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With tracing enabled, every Claude Desktop tool call and every Claude Code LLM request shows up as a trace with full metadata: model, tokens, latency, route, and any security policy actions.&lt;/p>
&lt;h2 id="testing-the-full-setup">Testing the Full Setup&lt;/h2>
&lt;h3 id="test-mcp-claude-desktop">Test MCP (Claude Desktop)&lt;/h3>
&lt;p>Open Claude Desktop and ask it to use the math tools:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;What&amp;rsquo;s 42 multiplied by 17?&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Claude should invoke the &lt;code>multiply&lt;/code> tool through agentgateway. Check the gateway logs to confirm the request was proxied.&lt;/p>
&lt;h3 id="test-llm-claude-code">Test LLM (Claude Code)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">ANTHROPIC_BASE_URL&lt;/span>&lt;span class="o">=&lt;/span>http://&lt;span class="nv">$GATEWAY_IP&lt;/span>:8080/anthropic claude
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># In Claude Code, type any prompt — it routes through the gateway to Anthropic&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-with-mcp-inspector">Test with MCP Inspector&lt;/h3>
&lt;p>You can also verify MCP connectivity directly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">npx @modelcontextprotocol/inspector
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Enter the URL: &lt;code>http://GATEWAY_IP:8080/mcp&lt;/code>&lt;/p>
&lt;p>You should see the math tools listed and be able to invoke them.&lt;/p>
&lt;h2 id="important-limitations">Important Limitations&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Claude Desktop&amp;rsquo;s core LLM traffic&lt;/strong> (conversations with Claude itself) cannot be routed through agentgateway — only MCP server traffic is proxied&lt;/li>
&lt;li>&lt;strong>Claude Code LLM traffic&lt;/strong> &lt;em>can&lt;/em> be fully routed through the gateway via the &lt;code>ANTHROPIC_BASE_URL&lt;/code> environment variable&lt;/li>
&lt;li>For local development (kind/minikube), you&amp;rsquo;ll need port-forwarding since there&amp;rsquo;s no external load balancer&lt;/li>
&lt;/ul>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>&lt;strong>MCP tools not showing in Claude Desktop?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Restart Claude Desktop after config changes&lt;/li>
&lt;li>Verify the gateway is reachable: &lt;code>curl http://GATEWAY_IP:8080/mcp&lt;/code>&lt;/li>
&lt;li>Check that supergateway/mcp-remote is installed: &lt;code>npx -y supergateway --help&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Claude Code connection refused?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Verify the gateway service has an external IP: &lt;code>kubectl get svc -n agentgateway-system&lt;/code>&lt;/li>
&lt;li>Check firewall rules allow traffic on port 8080&lt;/li>
&lt;li>Ensure &lt;code>ANTHROPIC_BASE_URL&lt;/code> is set correctly: &lt;code>echo $ANTHROPIC_BASE_URL&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Timeout errors?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>LLM requests can take time — ensure gateway timeouts are configured for AI workloads&lt;/li>
&lt;li>Check gateway pod resources are sufficient for the traffic volume&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Authentication errors?&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Verify the Anthropic API key secret exists: &lt;code>kubectl get secret anthropic-api-key -n agentgateway-system&lt;/code>&lt;/li>
&lt;li>Check the key is valid with a direct curl to Anthropic&amp;rsquo;s API&lt;/li>
&lt;/ul>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl delete -f gateway.yaml -f mcp-backend.yaml -f anthropic-backend.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete deployment mcp-math-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete svc mcp-math-server
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete configmap mcp-math-script
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret anthropic-api-key -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.solo.io/agentgateway/">Solo agentgateway Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway OSS&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anthropic.com/en/docs/claude-desktop/mcp">Claude Desktop MCP Documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code CLI&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/AdminTurnedDevOps/agentic-demo-repo/tree/main/agent-desktop-configs">Original demo configs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.anthropic.com/en/api">Anthropic API&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Getting Started with LLM Provider Routing on Kind</title><link>https://maniak.io/articles/llm-provider-routing-on-kind/</link><pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/llm-provider-routing-on-kind/</guid><description>&lt;p>Agentgateway makes it simple to route traffic to multiple LLM providers through a single gateway using the Kubernetes Gateway API. This guide walks through setting up agentgateway OSS on a local Kind cluster with xAI, Anthropic, and OpenAI backends, all routed through a listener named &lt;code>llm-providers&lt;/code>.&lt;/p>
&lt;p>One of the most common patterns in AI-native infrastructure is routing traffic to multiple LLM providers behind a single entry point. Whether you&amp;rsquo;re comparing models, building failover strategies, or just want a unified API across providers, agentgateway gives you a clean Kubernetes-native way to do it using the &lt;a href="https://gateway-api.sigs.k8s.io/">Gateway API&lt;/a> and &lt;code>AgentgatewayBackend&lt;/code> custom resources.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll set up a complete working example on a local &lt;a href="https://kind.sigs.k8s.io/">Kind&lt;/a> cluster with three LLM providers routed via path-based &lt;code>HTTPRoute&lt;/code> resources.&lt;/p>
&lt;h2 id="what-youll-build">What you&amp;rsquo;ll build&lt;/h2>
&lt;p>By the end of this guide you&amp;rsquo;ll have:&lt;/p>
&lt;ul>
&lt;li>A Kind cluster running the agentgateway control plane&lt;/li>
&lt;li>A &lt;code>Gateway&lt;/code> with a listener named &lt;code>llm-providers&lt;/code> on port 8080&lt;/li>
&lt;li>Three &lt;code>AgentgatewayBackend&lt;/code> resources for xAI, Anthropic, and OpenAI&lt;/li>
&lt;li>Three &lt;code>HTTPRoute&lt;/code> resources that route &lt;code>/xai&lt;/code>, &lt;code>/anthropic&lt;/code>, and &lt;code>/openai&lt;/code> to their respective backends&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/img/blog/llm-provider-routing/architecture-diagram.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/img/blog/llm-provider-routing/architecture-diagram.png" alt="agentgateway LLM Provider Routing Architecture" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>Before getting started, make sure you have the following installed:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.docker.com/">Docker&lt;/a> — container runtime for Kind&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/">Kind&lt;/a> — local Kubernetes clusters&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/docs/tasks/tools/">kubectl&lt;/a> — Kubernetes CLI (within 1 minor version of your cluster)&lt;/li>
&lt;li>&lt;a href="https://helm.sh/">Helm&lt;/a> — Kubernetes package manager&lt;/li>
&lt;/ul>
&lt;p>You also need API keys for the LLM providers you want to use. Export them as environment variables:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">XAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-xai-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-anthropic-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-openai-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind cluster&lt;/h2>
&lt;p>Create a local Kubernetes cluster using Kind. This gives you a lightweight, disposable cluster perfect for testing.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl cluster-info --context kind-agentgateway-demo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-agentgateway-oss-via-helm">Step 2: Install agentgateway OSS via Helm&lt;/h2>
&lt;h3 id="install-the-gateway-api-crds">Install the Gateway API CRDs&lt;/h3>
&lt;p>Agentgateway relies on the Kubernetes Gateway API. Install the standard CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-the-agentgateway-crds">Install the agentgateway CRDs&lt;/h3>
&lt;p>Install the custom resource definitions that agentgateway needs (&lt;code>AgentgatewayBackend&lt;/code>, &lt;code>AgentgatewayPolicy&lt;/code>, etc.):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0-main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-the-agentgateway-control-plane">Install the agentgateway control plane&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0-main &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.image.pullPolicy&lt;span class="o">=&lt;/span>Always
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>The &lt;code>--set controller.image.pullPolicy=Always&lt;/code> flag is recommended for development builds to ensure you always get the latest image.&lt;/p>
&lt;/blockquote>
&lt;p>Verify the pods are running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see the controller pod in a &lt;code>Running&lt;/code> state.&lt;/p>
&lt;h2 id="step-3-create-the-gateway">Step 3: Create the Gateway&lt;/h2>
&lt;p>The &lt;code>Gateway&lt;/code> resource is the entry point for all traffic. It defines a listener named &lt;code>llm-providers&lt;/code> on port 8080 that accepts &lt;code>HTTPRoute&lt;/code> resources from any namespace.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">infrastructure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parametersRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The listener name &lt;code>llm-providers&lt;/code> is the key here. All &lt;code>HTTPRoute&lt;/code> resources in the following steps reference this listener via &lt;code>sectionName&lt;/code>, so the gateway knows which listener should handle each route.&lt;/p>
&lt;h2 id="step-4-configure-api-key-secrets">Step 4: Configure API key secrets&lt;/h2>
&lt;p>Each LLM provider needs an API key stored as a Kubernetes &lt;code>Secret&lt;/code>. The &lt;code>AgentgatewayBackend&lt;/code> resources reference these secrets for authentication.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$XAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$ANTHROPIC_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>Never commit API keys to source control. Use environment variable substitution or a secrets manager in production.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-5-create-agentgateway-backends">Step 5: Create agentgateway backends&lt;/h2>
&lt;p>&lt;code>AgentgatewayBackend&lt;/code> resources define the LLM provider endpoints. Each backend specifies the provider type, model, and authentication. Agentgateway automatically rewrites requests to the correct chat completion endpoint for each provider.&lt;/p>
&lt;h3 id="xai-backend">xAI backend&lt;/h3>
&lt;p>xAI uses an OpenAI-compatible API. Because we&amp;rsquo;re specifying a custom host (&lt;code>api.x.ai&lt;/code>) rather than the default OpenAI host, we need to explicitly set the host, port, path, and TLS SNI.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4-1-fast-reasoning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/v1/chat/completions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="anthropic-backend">Anthropic backend&lt;/h3>
&lt;p>Anthropic uses its native provider type. Agentgateway handles the endpoint rewriting automatically — no custom host or TLS configuration needed.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;claude-sonnet-4-5-20250929&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="openai-backend">OpenAI backend&lt;/h3>
&lt;p>OpenAI also uses its native provider type with the default endpoint.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-create-httproutes">Step 6: Create HTTPRoutes&lt;/h2>
&lt;p>&lt;code>HTTPRoute&lt;/code> resources connect incoming request paths to the &lt;code>AgentgatewayBackend&lt;/code> resources. Each route references the &lt;code>llm-providers&lt;/code> listener on the Gateway via &lt;code>sectionName&lt;/code>, and matches a path prefix to direct traffic to the correct backend.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Route&lt;/th>
&lt;th>Path&lt;/th>
&lt;th>Backend&lt;/th>
&lt;th>Provider&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>xai&lt;/td>
&lt;td>&lt;code>/xai&lt;/code>&lt;/td>
&lt;td>xai&lt;/td>
&lt;td>xAI (Grok)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>anthropic&lt;/td>
&lt;td>&lt;code>/anthropic&lt;/code>&lt;/td>
&lt;td>anthropic&lt;/td>
&lt;td>Anthropic (Claude)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>openai&lt;/td>
&lt;td>&lt;code>/openai&lt;/code>&lt;/td>
&lt;td>openai&lt;/td>
&lt;td>OpenAI (GPT)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="xai-route">xAI route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route-type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="anthropic-route">Anthropic route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route-type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="openai-route">OpenAI route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route-type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key fields that tie everything together:&lt;/p>
&lt;ul>
&lt;li>&lt;code>parentRefs.sectionName: llm-providers&lt;/code> — binds the route to the specific Gateway listener&lt;/li>
&lt;li>&lt;code>backendRefs.group: agentgateway.dev&lt;/code> — tells the Gateway API to look for &lt;code>AgentgatewayBackend&lt;/code> resources (not standard Kubernetes &lt;code>Service&lt;/code> objects)&lt;/li>
&lt;li>&lt;code>backendRefs.kind: AgentgatewayBackend&lt;/code> — references the custom backend type&lt;/li>
&lt;li>&lt;code>labels.route-type: llm-provider&lt;/code> — optional label useful for filtering and grouping&lt;/li>
&lt;/ul>
&lt;h2 id="step-7-verify-and-test">Step 7: Verify and test&lt;/h2>
&lt;p>Once all resources are applied, verify everything is connected and working.&lt;/p>
&lt;h3 id="check-resource-status">Check resource status&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl get gateway -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify backends exist&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify routes are attached&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="port-forward-and-test">Port-forward and test&lt;/h3>
&lt;p>Forward the gateway port to your local machine and send a test request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> svc/agentgateway-proxy 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the OpenAI route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8080/openai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the Anthropic route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8080/anthropic &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;claude-sonnet-4-5-20250929&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the xAI route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8080/xai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;grok-4-1-fast-reasoning&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agentgateway automatically rewrites requests to each provider&amp;rsquo;s chat completion endpoint, so you use a unified request format regardless of the backend provider.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done, remove everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove routes and backends&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute xai anthropic openai -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend xai anthropic openai -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret xai-secret anthropic-secret openai-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Uninstall Helm charts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall agentgateway agentgateway-crds -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the Kind cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="whats-next">What&amp;rsquo;s next&lt;/h2>
&lt;p>Now that you have path-based LLM routing working, there&amp;rsquo;s a lot more you can do with agentgateway:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Multiple providers on one route&lt;/strong> — group backends for automatic load balancing and failover. Agentgateway picks two random providers and selects the healthiest one.&lt;/li>
&lt;li>&lt;strong>Prompt guarding&lt;/strong> — add &lt;code>AgentgatewayPolicy&lt;/code> resources for regex-based prompt filtering or webhook-based validation before requests hit your LLM.&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong> — protect your API keys and budgets with local or remote rate limiting policies.&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — enable full &lt;a href="https://opentelemetry.io/">OpenTelemetry&lt;/a> support for metrics, logs, and distributed tracing across all your LLM traffic.&lt;/li>
&lt;/ul>
&lt;p>Check out the &lt;a href="https://agentgateway.dev/docs/kubernetes/latest/quickstart/">agentgateway docs&lt;/a> for more, or come chat with us on &lt;a href="https://discord.com/invite/y9efgEmppm">Discord&lt;/a>.&lt;/p>
&lt;blockquote>
&lt;p>With a single Gateway listener and a few YAML resources, you get a unified, Kubernetes-native control point for all your LLM traffic. That&amp;rsquo;s the power of agentgateway.&lt;/p>
&lt;/blockquote></description><content:encoded>&lt;p>Agentgateway makes it simple to route traffic to multiple LLM providers through a single gateway using the Kubernetes Gateway API. This guide walks through setting up agentgateway OSS on a local Kind cluster with xAI, Anthropic, and OpenAI backends, all routed through a listener named &lt;code>llm-providers&lt;/code>.&lt;/p>
&lt;p>One of the most common patterns in AI-native infrastructure is routing traffic to multiple LLM providers behind a single entry point. Whether you&amp;rsquo;re comparing models, building failover strategies, or just want a unified API across providers, agentgateway gives you a clean Kubernetes-native way to do it using the &lt;a href="https://gateway-api.sigs.k8s.io/">Gateway API&lt;/a> and &lt;code>AgentgatewayBackend&lt;/code> custom resources.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll set up a complete working example on a local &lt;a href="https://kind.sigs.k8s.io/">Kind&lt;/a> cluster with three LLM providers routed via path-based &lt;code>HTTPRoute&lt;/code> resources.&lt;/p>
&lt;h2 id="what-youll-build">What you&amp;rsquo;ll build&lt;/h2>
&lt;p>By the end of this guide you&amp;rsquo;ll have:&lt;/p>
&lt;ul>
&lt;li>A Kind cluster running the agentgateway control plane&lt;/li>
&lt;li>A &lt;code>Gateway&lt;/code> with a listener named &lt;code>llm-providers&lt;/code> on port 8080&lt;/li>
&lt;li>Three &lt;code>AgentgatewayBackend&lt;/code> resources for xAI, Anthropic, and OpenAI&lt;/li>
&lt;li>Three &lt;code>HTTPRoute&lt;/code> resources that route &lt;code>/xai&lt;/code>, &lt;code>/anthropic&lt;/code>, and &lt;code>/openai&lt;/code> to their respective backends&lt;/li>
&lt;/ul>
&lt;p>&lt;a class="mk-zoom" href="https://maniak.io/img/blog/llm-provider-routing/architecture-diagram.png" aria-label="Open full-size image">
 &lt;img src="https://maniak.io/img/blog/llm-provider-routing/architecture-diagram.png" alt="agentgateway LLM Provider Routing Architecture" loading="lazy">
&lt;/a>
&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>Before getting started, make sure you have the following installed:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.docker.com/">Docker&lt;/a> — container runtime for Kind&lt;/li>
&lt;li>&lt;a href="https://kind.sigs.k8s.io/">Kind&lt;/a> — local Kubernetes clusters&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/docs/tasks/tools/">kubectl&lt;/a> — Kubernetes CLI (within 1 minor version of your cluster)&lt;/li>
&lt;li>&lt;a href="https://helm.sh/">Helm&lt;/a> — Kubernetes package manager&lt;/li>
&lt;/ul>
&lt;p>You also need API keys for the LLM providers you want to use. Export them as environment variables:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">XAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-xai-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ANTHROPIC_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-anthropic-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-openai-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-1-create-a-kind-cluster">Step 1: Create a Kind cluster&lt;/h2>
&lt;p>Create a local Kubernetes cluster using Kind. This gives you a lightweight, disposable cluster perfect for testing.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl cluster-info --context kind-agentgateway-demo
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-install-agentgateway-oss-via-helm">Step 2: Install agentgateway OSS via Helm&lt;/h2>
&lt;h3 id="install-the-gateway-api-crds">Install the Gateway API CRDs&lt;/h3>
&lt;p>Agentgateway relies on the Kubernetes Gateway API. Install the standard CRDs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-the-agentgateway-crds">Install the agentgateway CRDs&lt;/h3>
&lt;p>Install the custom resource definitions that agentgateway needs (&lt;code>AgentgatewayBackend&lt;/code>, &lt;code>AgentgatewayPolicy&lt;/code>, etc.):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0-main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-the-agentgateway-control-plane">Install the agentgateway control plane&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0-main &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set controller.image.pullPolicy&lt;span class="o">=&lt;/span>Always
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>The &lt;code>--set controller.image.pullPolicy=Always&lt;/code> flag is recommended for development builds to ensure you always get the latest image.&lt;/p>
&lt;/blockquote>
&lt;p>Verify the pods are running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should see the controller pod in a &lt;code>Running&lt;/code> state.&lt;/p>
&lt;h2 id="step-3-create-the-gateway">Step 3: Create the Gateway&lt;/h2>
&lt;p>The &lt;code>Gateway&lt;/code> resource is the entry point for all traffic. It defines a listener named &lt;code>llm-providers&lt;/code> on port 8080 that accepts &lt;code>HTTPRoute&lt;/code> resources from any namespace.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterprise-agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">infrastructure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parametersRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTP&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">All&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The listener name &lt;code>llm-providers&lt;/code> is the key here. All &lt;code>HTTPRoute&lt;/code> resources in the following steps reference this listener via &lt;code>sectionName&lt;/code>, so the gateway knows which listener should handle each route.&lt;/p>
&lt;h2 id="step-4-configure-api-key-secrets">Step 4: Configure API key secrets&lt;/h2>
&lt;p>Each LLM provider needs an API key stored as a Kubernetes &lt;code>Secret&lt;/code>. The &lt;code>AgentgatewayBackend&lt;/code> resources reference these secrets for authentication.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$XAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$ANTHROPIC_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Opaque&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">$OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>Never commit API keys to source control. Use environment variable substitution or a secrets manager in production.&lt;/p>
&lt;/blockquote>
&lt;h2 id="step-5-create-agentgateway-backends">Step 5: Create agentgateway backends&lt;/h2>
&lt;p>&lt;code>AgentgatewayBackend&lt;/code> resources define the LLM provider endpoints. Each backend specifies the provider type, model, and authentication. Agentgateway automatically rewrites requests to the correct chat completion endpoint for each provider.&lt;/p>
&lt;h3 id="xai-backend">xAI backend&lt;/h3>
&lt;p>xAI uses an OpenAI-compatible API. Because we&amp;rsquo;re specifying a custom host (&lt;code>api.x.ai&lt;/code>) rather than the default OpenAI host, we need to explicitly set the host, port, path, and TLS SNI.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grok-4-1-fast-reasoning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">host&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">443&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/v1/chat/completions&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sni&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">api.x.ai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="anthropic-backend">Anthropic backend&lt;/h3>
&lt;p>Anthropic uses its native provider type. Agentgateway handles the endpoint rewriting automatically — no custom host or TLS configuration needed.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">anthropic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;claude-sonnet-4-5-20250929&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="openai-backend">OpenAI backend&lt;/h3>
&lt;p>OpenAI also uses its native provider type with the default endpoint.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o-mini&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">auth&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-6-create-httproutes">Step 6: Create HTTPRoutes&lt;/h2>
&lt;p>&lt;code>HTTPRoute&lt;/code> resources connect incoming request paths to the &lt;code>AgentgatewayBackend&lt;/code> resources. Each route references the &lt;code>llm-providers&lt;/code> listener on the Gateway via &lt;code>sectionName&lt;/code>, and matches a path prefix to direct traffic to the correct backend.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Route&lt;/th>
&lt;th>Path&lt;/th>
&lt;th>Backend&lt;/th>
&lt;th>Provider&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>xai&lt;/td>
&lt;td>&lt;code>/xai&lt;/code>&lt;/td>
&lt;td>xai&lt;/td>
&lt;td>xAI (Grok)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>anthropic&lt;/td>
&lt;td>&lt;code>/anthropic&lt;/code>&lt;/td>
&lt;td>anthropic&lt;/td>
&lt;td>Anthropic (Claude)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>openai&lt;/td>
&lt;td>&lt;code>/openai&lt;/code>&lt;/td>
&lt;td>openai&lt;/td>
&lt;td>OpenAI (GPT)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="xai-route">xAI route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route-type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">xai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="anthropic-route">Anthropic route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route-type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">anthropic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="openai-route">OpenAI route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="l">kubectl apply -f- &amp;lt;&amp;lt;EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">route-type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-provider&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-proxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sectionName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm-providers&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="l">EOF&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The key fields that tie everything together:&lt;/p>
&lt;ul>
&lt;li>&lt;code>parentRefs.sectionName: llm-providers&lt;/code> — binds the route to the specific Gateway listener&lt;/li>
&lt;li>&lt;code>backendRefs.group: agentgateway.dev&lt;/code> — tells the Gateway API to look for &lt;code>AgentgatewayBackend&lt;/code> resources (not standard Kubernetes &lt;code>Service&lt;/code> objects)&lt;/li>
&lt;li>&lt;code>backendRefs.kind: AgentgatewayBackend&lt;/code> — references the custom backend type&lt;/li>
&lt;li>&lt;code>labels.route-type: llm-provider&lt;/code> — optional label useful for filtering and grouping&lt;/li>
&lt;/ul>
&lt;h2 id="step-7-verify-and-test">Step 7: Verify and test&lt;/h2>
&lt;p>Once all resources are applied, verify everything is connected and working.&lt;/p>
&lt;h3 id="check-resource-status">Check resource status&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl get gateway -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify backends exist&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify routes are attached&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="port-forward-and-test">Port-forward and test&lt;/h3>
&lt;p>Forward the gateway port to your local machine and send a test request:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> svc/agentgateway-proxy 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the OpenAI route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8080/openai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the Anthropic route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8080/anthropic &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;claude-sonnet-4-5-20250929&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Test the xAI route:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">curl -s http://localhost:8080/xai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;grok-4-1-fast-reasoning&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Agentgateway automatically rewrites requests to each provider&amp;rsquo;s chat completion endpoint, so you use a unified request format regardless of the backend provider.&lt;/p>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done, remove everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove routes and backends&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute xai anthropic openai -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend xai anthropic openai -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret xai-secret anthropic-secret openai-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Uninstall Helm charts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall agentgateway agentgateway-crds -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the Kind cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway-demo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="whats-next">What&amp;rsquo;s next&lt;/h2>
&lt;p>Now that you have path-based LLM routing working, there&amp;rsquo;s a lot more you can do with agentgateway:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Multiple providers on one route&lt;/strong> — group backends for automatic load balancing and failover. Agentgateway picks two random providers and selects the healthiest one.&lt;/li>
&lt;li>&lt;strong>Prompt guarding&lt;/strong> — add &lt;code>AgentgatewayPolicy&lt;/code> resources for regex-based prompt filtering or webhook-based validation before requests hit your LLM.&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong> — protect your API keys and budgets with local or remote rate limiting policies.&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> — enable full &lt;a href="https://opentelemetry.io/">OpenTelemetry&lt;/a> support for metrics, logs, and distributed tracing across all your LLM traffic.&lt;/li>
&lt;/ul>
&lt;p>Check out the &lt;a href="https://agentgateway.dev/docs/kubernetes/latest/quickstart/">agentgateway docs&lt;/a> for more, or come chat with us on &lt;a href="https://discord.com/invite/y9efgEmppm">Discord&lt;/a>.&lt;/p>
&lt;blockquote>
&lt;p>With a single Gateway listener and a few YAML resources, you get a unified, Kubernetes-native control point for all your LLM traffic. That&amp;rsquo;s the power of agentgateway.&lt;/p>
&lt;/blockquote></content:encoded></item><item><title>Open Source LLM Observability: Tracing AI Calls with agentgateway and Langfuse</title><link>https://maniak.io/articles/2026-02-14-llm-observability-agentgateway-langfuse/</link><pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-14-llm-observability-agentgateway-langfuse/</guid><description>&lt;p>Your AI agents are calling LLMs hundreds of times a day. Do you know what they&amp;rsquo;re sending? What they&amp;rsquo;re spending? Whether that prompt injection guard actually fired?&lt;/p>
&lt;p>This guide shows how to wire &lt;a href="https://langfuse.com">Langfuse&lt;/a> into &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> to get &lt;strong>full observability over every LLM and MCP tool call&lt;/strong> — zero application code changes required.&lt;/p>
&lt;hr>
&lt;h2 id="why-gateway-level-observability">Why Gateway-Level Observability?&lt;/h2>
&lt;p>Most teams add tracing inside their application code — wrapping LLM SDK calls with Langfuse decorators or OpenTelemetry spans. This works, but it has gaps:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You only see what the app reports.&lt;/strong> If a developer forgets to instrument a call, it&amp;rsquo;s invisible.&lt;/li>
&lt;li>&lt;strong>Gateway-level policies are opaque.&lt;/strong> PII redaction, prompt injection blocking, rate limiting — these happen at the gateway. Application-level tracing can&amp;rsquo;t see them.&lt;/li>
&lt;li>&lt;strong>Multiple agents, multiple codebases.&lt;/strong> Every team has to add their own instrumentation. Different languages, different quality.&lt;/li>
&lt;/ul>
&lt;p>When tracing happens at the &lt;strong>gateway layer&lt;/strong>, every request is captured automatically. Every agent, every provider, every tool call — same fidelity, same format, zero code changes.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐ ┌────────────────────────┐ ┌─────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Your App / │ │ Solo agentgateway │ │ LLM Provider │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ AI Agent │────▶│ (Gateway API) │────▶│ (OpenAI, etc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ └───────────┬────────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP Traces (gRPC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌───────────▼────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenTelemetry │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Collector (fan-out) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────┬───────────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP HTTP OTLP gRPC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌─────▼──┐ ┌─────▼──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Langfuse│ │ClickHouse / │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ UI │ │ Solo Enterprise │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ UI (optional) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway natively emits OpenTelemetry traces for every LLM request. A lightweight OTel Collector receives these traces and forwards them to Langfuse via OTLP HTTP. The same collector can fan-out traces to additional backends (ClickHouse, Jaeger, Datadog) simultaneously.&lt;/p>
&lt;hr>
&lt;h2 id="quick-start-kind-cluster--agentgateway-21-oss">Quick Start: Kind Cluster + agentgateway 2.1 OSS&lt;/h2>
&lt;p>Don&amp;rsquo;t have a cluster? Here&amp;rsquo;s the fastest path from zero to traced LLM calls.&lt;/p>
&lt;h3 id="create-the-cluster">Create the Cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-gateway-api-crds">Install Gateway API CRDs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-agentgateway-21-oss">Install agentgateway 2.1 OSS&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway-crds oci://cr.agentgateway.dev/helm/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install control plane&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gatewayclass agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="setting-up-langfuse-tracing">Setting Up Langfuse Tracing&lt;/h2>
&lt;h3 id="step-1-get-langfuse-api-keys">Step 1: Get Langfuse API Keys&lt;/h3>
&lt;p>Sign up at &lt;a href="https://cloud.langfuse.com">cloud.langfuse.com&lt;/a> (free tier) or use a self-hosted instance. Go to &lt;strong>Settings → API Keys&lt;/strong> and create a key pair.&lt;/p>
&lt;p>Base64 encode your credentials:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;pk-lf-YOUR_PUBLIC_KEY:sk-lf-YOUR_SECRET_KEY&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> base64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-2-deploy-the-otel-collector">Step 2: Deploy the OTel Collector&lt;/h3>
&lt;p>The collector bridges agentgateway&amp;rsquo;s OTLP gRPC output to Langfuse&amp;rsquo;s OTLP HTTP input:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config.yaml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4318
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlphttp/langfuse:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: http://cloud.langfuse.com/api/public/otel
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Authorization: &amp;#34;Basic &amp;lt;YOUR_BASE64_CREDENTIALS&amp;gt;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> retry_on_failure:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> initial_interval: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_interval: 30s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_elapsed_time: 300s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> batch:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> send_batch_size: 1000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> timeout: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> pipelines:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> traces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers: [otlp]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors: [batch]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters: [otlphttp/langfuse]&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib:0.132.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--config=/conf/config.yaml&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f langfuse-collector.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-3-configure-agentgateway-tracing">Step 3: Configure agentgateway Tracing&lt;/h3>
&lt;p>For agentgateway OSS, enable tracing via Helm values:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values-tracing.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values-tracing.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For &lt;strong>agentgateway Enterprise&lt;/strong>, use the &lt;code>EnterpriseAgentgatewayParameters&lt;/code> resource instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.operation.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;chat&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.provider&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.requestModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.response.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.responseModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.prompt_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.inputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.completion_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.outputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.total_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.totalTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.temperature&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.params.temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.prompt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.completion&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-4-create-a-gateway-and-route">Step 4: Create a Gateway and Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Same&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the API key secret&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-api-key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Apply the gateway and route&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-5-test-and-view-traces">Step 5: Test and View Traces&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Port-forward the gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/ai-gateway 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Send a test request&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:8080/openai/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4.1-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from agentgateway!&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open Langfuse → &lt;strong>Traces&lt;/strong>. You should see a trace with model, tokens, prompt, completion, and gateway metadata.&lt;/p>
&lt;hr>
&lt;h2 id="what-gets-captured">What Gets Captured&lt;/h2>
&lt;h3 id="trace-attributes-genai-semantic-conventions">Trace Attributes (GenAI Semantic Conventions)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gen_ai.system&lt;/code>&lt;/td>
&lt;td>LLM provider&lt;/td>
&lt;td>&lt;code>openai&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>&lt;/td>
&lt;td>Requested model&lt;/td>
&lt;td>&lt;code>gpt-4.1-mini&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.model&lt;/code>&lt;/td>
&lt;td>Actual model used&lt;/td>
&lt;td>&lt;code>gpt-4o-mini-2024-07-18&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.prompt_tokens&lt;/code>&lt;/td>
&lt;td>Input tokens&lt;/td>
&lt;td>&lt;code>13&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.completion_tokens&lt;/code>&lt;/td>
&lt;td>Output tokens&lt;/td>
&lt;td>&lt;code>30&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.total_tokens&lt;/code>&lt;/td>
&lt;td>Combined&lt;/td>
&lt;td>&lt;code>43&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt&lt;/code>&lt;/td>
&lt;td>Full prompt content&lt;/td>
&lt;td>&lt;code>[{&amp;quot;role&amp;quot;:&amp;quot;user&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;Hello&amp;quot;}]&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.completion&lt;/code>&lt;/td>
&lt;td>Full completion&lt;/td>
&lt;td>&lt;code>Hello! How can I help you?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.streaming&lt;/code>&lt;/td>
&lt;td>Streaming used&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="gateway-metadata">Gateway Metadata&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gateway&lt;/code>&lt;/td>
&lt;td>Gateway resource&lt;/td>
&lt;td>&lt;code>agentgateway-system/ai-gateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>route&lt;/code>&lt;/td>
&lt;td>HTTPRoute name&lt;/td>
&lt;td>&lt;code>agentgateway-system/openai&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>endpoint&lt;/code>&lt;/td>
&lt;td>Backend endpoint&lt;/td>
&lt;td>&lt;code>api.openai.com:443&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>listener&lt;/code>&lt;/td>
&lt;td>Gateway listener&lt;/td>
&lt;td>&lt;code>llm&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="multi-provider-support">Multi-Provider Support&lt;/h2>
&lt;p>agentgateway traces all providers through the same pipeline — OpenAI, Anthropic, xAI/Grok, Azure OpenAI, Google Gemini, Ollama, and any OpenAI-compatible API. Add more routes, same observability:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">/openai/* → OpenAI GPT → traced to Langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/anthropic/* → Anthropic → traced to Langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/xai/* → xAI Grok → traced to Langfuse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every provider, same trace format, same dashboard.&lt;/p>
&lt;hr>
&lt;h2 id="fan-out-langfuse--additional-backends">Fan-Out: Langfuse + Additional Backends&lt;/h2>
&lt;p>Send traces to multiple backends simultaneously:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Basic &amp;lt;CREDENTIALS&amp;gt;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp/jaeger&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jaeger-collector:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse, otlp/jaeger]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Common fan-out targets: Langfuse (LLM analytics) + ClickHouse (gateway metrics) + Jaeger (distributed tracing) + Datadog (enterprise monitoring).&lt;/p>
&lt;hr>
&lt;h2 id="mcp-tool-tracing">MCP Tool Tracing&lt;/h2>
&lt;p>agentgateway doesn&amp;rsquo;t just trace LLM calls — it also traces MCP (Model Context Protocol) tool interactions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tool discovery&lt;/strong> (&lt;code>tools/list&lt;/code>) — which tools are available, how long discovery takes&lt;/li>
&lt;li>&lt;strong>Tool execution&lt;/strong> (&lt;code>tools/call&lt;/code>) — parameters, results, latency&lt;/li>
&lt;li>&lt;strong>Backend MCP server performance&lt;/strong> — per-server latency and error rates&lt;/li>
&lt;/ul>
&lt;p>When an agent calls Slack, GitHub, or any MCP tool server through agentgateway, the full tool call chain appears in Langfuse alongside the LLM calls that triggered it.&lt;/p>
&lt;hr>
&lt;h2 id="security-policy-visibility">Security Policy Visibility&lt;/h2>
&lt;p>When agentgateway&amp;rsquo;s security policies fire, the trace metadata includes what happened:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PII Protection&lt;/strong> — how many entities were redacted, what types (email, SSN, phone)&lt;/li>
&lt;li>&lt;strong>Prompt Injection&lt;/strong> — whether an injection was detected and blocked&lt;/li>
&lt;li>&lt;strong>Credential Leak&lt;/strong> — whether secrets were caught in the LLM response&lt;/li>
&lt;li>&lt;strong>Rate Limiting&lt;/strong> — remaining quota for the user&lt;/li>
&lt;/ul>
&lt;p>This means you can see not just &lt;em>what&lt;/em> your agents are doing, but &lt;em>what guardrails are protecting them&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Problem&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>No traces in Langfuse&lt;/td>
&lt;td>Check collector pod is running: &lt;code>kubectl get pods -n agentgateway-system -l app=langfuse-otel-collector&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>401 errors in collector logs&lt;/td>
&lt;td>Wrong Langfuse API credentials — re-check the base64 encoding&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Traces show but no prompt/completion&lt;/td>
&lt;td>Add the &lt;code>fields.add&lt;/code> section in Enterprise, or check OTEL env vars in OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Missing gateway metadata&lt;/td>
&lt;td>Restart proxies after config change: &lt;code>kubectl rollout restart deployment -n agentgateway-system&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="full-source-code">Full Source Code&lt;/h2>
&lt;p>All manifests, configs, and examples are in the &lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse">agentgateway-langfuse&lt;/a> repository:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/blob/main/docs/quickstart-kind.md">docs/quickstart-kind.md&lt;/a>&lt;/td>
&lt;td>Full kind cluster quickstart guide&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/tree/main/examples/basic">examples/basic/&lt;/a>&lt;/td>
&lt;td>Basic Langfuse collector + tracing config&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/tree/main/examples/fan-out">examples/fan-out/&lt;/a>&lt;/td>
&lt;td>Fan-out to Langfuse + additional backends&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/tree/main/examples/argocd">examples/argocd/&lt;/a>&lt;/td>
&lt;td>Production ArgoCD/GitOps deployment&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/blob/main/scripts/verify.sh">scripts/verify.sh&lt;/a>&lt;/td>
&lt;td>End-to-end verification script&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway OSS&lt;/a> — CNCF open-source AI gateway&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/kubernetes/latest/install/helm/">agentgateway Helm Install&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com">Langfuse&lt;/a> — Open-source LLM observability&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs/integrations/opentelemetry">Langfuse OpenTelemetry Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI Conventions&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded>&lt;p>Your AI agents are calling LLMs hundreds of times a day. Do you know what they&amp;rsquo;re sending? What they&amp;rsquo;re spending? Whether that prompt injection guard actually fired?&lt;/p>
&lt;p>This guide shows how to wire &lt;a href="https://langfuse.com">Langfuse&lt;/a> into &lt;a href="https://agentgateway.dev">agentgateway&lt;/a> to get &lt;strong>full observability over every LLM and MCP tool call&lt;/strong> — zero application code changes required.&lt;/p>
&lt;hr>
&lt;h2 id="why-gateway-level-observability">Why Gateway-Level Observability?&lt;/h2>
&lt;p>Most teams add tracing inside their application code — wrapping LLM SDK calls with Langfuse decorators or OpenTelemetry spans. This works, but it has gaps:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You only see what the app reports.&lt;/strong> If a developer forgets to instrument a call, it&amp;rsquo;s invisible.&lt;/li>
&lt;li>&lt;strong>Gateway-level policies are opaque.&lt;/strong> PII redaction, prompt injection blocking, rate limiting — these happen at the gateway. Application-level tracing can&amp;rsquo;t see them.&lt;/li>
&lt;li>&lt;strong>Multiple agents, multiple codebases.&lt;/strong> Every team has to add their own instrumentation. Different languages, different quality.&lt;/li>
&lt;/ul>
&lt;p>When tracing happens at the &lt;strong>gateway layer&lt;/strong>, every request is captured automatically. Every agent, every provider, every tool call — same fidelity, same format, zero code changes.&lt;/p>
&lt;hr>
&lt;h2 id="architecture">Architecture&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">┌──────────────┐ ┌────────────────────────┐ ┌─────────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ Your App / │ │ Solo agentgateway │ │ LLM Provider │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ AI Agent │────▶│ (Gateway API) │────▶│ (OpenAI, etc) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">│ │ │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└──────────────┘ └───────────┬────────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP Traces (gRPC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌───────────▼────────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ OpenTelemetry │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ Collector (fan-out) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─────┬───────────┬──────┘
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTLP HTTP OTLP gRPC
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ┌─────▼──┐ ┌─────▼──────────┐
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │Langfuse│ │ClickHouse / │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ UI │ │ Solo Enterprise │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> │ │ │ UI (optional) │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └────────┘ └─────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>agentgateway natively emits OpenTelemetry traces for every LLM request. A lightweight OTel Collector receives these traces and forwards them to Langfuse via OTLP HTTP. The same collector can fan-out traces to additional backends (ClickHouse, Jaeger, Datadog) simultaneously.&lt;/p>
&lt;hr>
&lt;h2 id="quick-start-kind-cluster--agentgateway-21-oss">Quick Start: Kind Cluster + agentgateway 2.1 OSS&lt;/h2>
&lt;p>Don&amp;rsquo;t have a cluster? Here&amp;rsquo;s the fastest path from zero to traced LLM calls.&lt;/p>
&lt;h3 id="create-the-cluster">Create the Cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-gateway-api-crds">Install Gateway API CRDs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-agentgateway-21-oss">Install agentgateway 2.1 OSS&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway-crds oci://cr.agentgateway.dev/helm/agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install control plane&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verify it&amp;rsquo;s running:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gatewayclass agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="setting-up-langfuse-tracing">Setting Up Langfuse Tracing&lt;/h2>
&lt;h3 id="step-1-get-langfuse-api-keys">Step 1: Get Langfuse API Keys&lt;/h3>
&lt;p>Sign up at &lt;a href="https://cloud.langfuse.com">cloud.langfuse.com&lt;/a> (free tier) or use a self-hosted instance. Go to &lt;strong>Settings → API Keys&lt;/strong> and create a key pair.&lt;/p>
&lt;p>Base64 encode your credentials:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> -n &lt;span class="s2">&amp;#34;pk-lf-YOUR_PUBLIC_KEY:sk-lf-YOUR_SECRET_KEY&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> base64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-2-deploy-the-otel-collector">Step 2: Deploy the OTel Collector&lt;/h3>
&lt;p>The collector bridges agentgateway&amp;rsquo;s OTLP gRPC output to Langfuse&amp;rsquo;s OTLP HTTP input:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ConfigMap&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config.yaml&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: 0.0.0.0:4318
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> otlphttp/langfuse:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> endpoint: http://cloud.langfuse.com/api/public/otel
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Authorization: &amp;#34;Basic &amp;lt;YOUR_BASE64_CREDENTIALS&amp;gt;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> retry_on_failure:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> initial_interval: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_interval: 30s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> max_elapsed_time: 300s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> batch:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> send_batch_size: 1000
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> timeout: 5s
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> pipelines:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> traces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> receivers: [otlp]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> processors: [batch]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> exporters: [otlphttp/langfuse]&lt;/span>&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otel/opentelemetry-collector-contrib:0.132.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--config=/conf/config.yaml&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumeMounts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/conf&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">50m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">128Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">200m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">256Mi&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">configMap&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector-config&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">langfuse-otel-collector&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">otlp-http&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f langfuse-collector.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-3-configure-agentgateway-tracing">Step 3: Configure agentgateway Tracing&lt;/h3>
&lt;p>For agentgateway OSS, enable tracing via Helm values:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values-tracing.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">gateway&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">envs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;grpc&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade agentgateway oci://cr.agentgateway.dev/helm/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version 2.1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f values-tracing.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For &lt;strong>agentgateway Enterprise&lt;/strong>, use the &lt;code>EnterpriseAgentgatewayParameters&lt;/code> resource instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">enterpriseagentgateway.solo.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">EnterpriseAgentgatewayParameters&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tracing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tracing&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpEndpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc://langfuse-otel-collector.agentgateway-system.svc.cluster.local:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlpProtocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">grpc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">randomSampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fields&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">add&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.operation.name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;&amp;#34;chat&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.system&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.provider&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.requestModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.response.model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.responseModel&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.prompt_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.inputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.completion_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.outputTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.usage.total_tokens&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.totalTokens&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.request.temperature&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.params.temperature&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.prompt&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.prompt&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen_ai.completion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;llm.completion&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-4-create-a-gateway-and-route">Step 4: Create a Gateway and Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gatewayClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">listeners&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocol&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPS&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">allowedRoutes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespaces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">from&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Same&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gateway.networking.k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HTTPRoute&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">parentRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ai-gateway&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">matches&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">PathPrefix&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">backendRefs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">group&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.dev/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AgentgatewayBackend&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">llm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">openai&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">authToken&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">secretRef&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai-api-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway-system&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the API key secret&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-api-key &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="nv">Authorization&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Apply the gateway and route&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f gateway.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="step-5-test-and-view-traces">Step 5: Test and View Traces&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Port-forward the gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/ai-gateway 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Send a test request&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -X POST http://localhost:8080/openai/v1/chat/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4.1-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from agentgateway!&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Open Langfuse → &lt;strong>Traces&lt;/strong>. You should see a trace with model, tokens, prompt, completion, and gateway metadata.&lt;/p>
&lt;hr>
&lt;h2 id="what-gets-captured">What Gets Captured&lt;/h2>
&lt;h3 id="trace-attributes-genai-semantic-conventions">Trace Attributes (GenAI Semantic Conventions)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gen_ai.system&lt;/code>&lt;/td>
&lt;td>LLM provider&lt;/td>
&lt;td>&lt;code>openai&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>&lt;/td>
&lt;td>Requested model&lt;/td>
&lt;td>&lt;code>gpt-4.1-mini&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.model&lt;/code>&lt;/td>
&lt;td>Actual model used&lt;/td>
&lt;td>&lt;code>gpt-4o-mini-2024-07-18&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.prompt_tokens&lt;/code>&lt;/td>
&lt;td>Input tokens&lt;/td>
&lt;td>&lt;code>13&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.completion_tokens&lt;/code>&lt;/td>
&lt;td>Output tokens&lt;/td>
&lt;td>&lt;code>30&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.total_tokens&lt;/code>&lt;/td>
&lt;td>Combined&lt;/td>
&lt;td>&lt;code>43&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt&lt;/code>&lt;/td>
&lt;td>Full prompt content&lt;/td>
&lt;td>&lt;code>[{&amp;quot;role&amp;quot;:&amp;quot;user&amp;quot;,&amp;quot;content&amp;quot;:&amp;quot;Hello&amp;quot;}]&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.completion&lt;/code>&lt;/td>
&lt;td>Full completion&lt;/td>
&lt;td>&lt;code>Hello! How can I help you?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.streaming&lt;/code>&lt;/td>
&lt;td>Streaming used&lt;/td>
&lt;td>&lt;code>false&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="gateway-metadata">Gateway Metadata&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gateway&lt;/code>&lt;/td>
&lt;td>Gateway resource&lt;/td>
&lt;td>&lt;code>agentgateway-system/ai-gateway&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>route&lt;/code>&lt;/td>
&lt;td>HTTPRoute name&lt;/td>
&lt;td>&lt;code>agentgateway-system/openai&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>endpoint&lt;/code>&lt;/td>
&lt;td>Backend endpoint&lt;/td>
&lt;td>&lt;code>api.openai.com:443&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>listener&lt;/code>&lt;/td>
&lt;td>Gateway listener&lt;/td>
&lt;td>&lt;code>llm&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="multi-provider-support">Multi-Provider Support&lt;/h2>
&lt;p>agentgateway traces all providers through the same pipeline — OpenAI, Anthropic, xAI/Grok, Azure OpenAI, Google Gemini, Ollama, and any OpenAI-compatible API. Add more routes, same observability:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">/openai/* → OpenAI GPT → traced to Langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/anthropic/* → Anthropic → traced to Langfuse
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/xai/* → xAI Grok → traced to Langfuse
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every provider, same trace format, same dashboard.&lt;/p>
&lt;hr>
&lt;h2 id="fan-out-langfuse--additional-backends">Fan-Out: Langfuse + Additional Backends&lt;/h2>
&lt;p>Send traces to multiple backends simultaneously:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://cloud.langfuse.com/api/public/otel&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Basic &amp;lt;CREDENTIALS&amp;gt;&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp/jaeger&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">jaeger-collector:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse, otlp/jaeger]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Common fan-out targets: Langfuse (LLM analytics) + ClickHouse (gateway metrics) + Jaeger (distributed tracing) + Datadog (enterprise monitoring).&lt;/p>
&lt;hr>
&lt;h2 id="mcp-tool-tracing">MCP Tool Tracing&lt;/h2>
&lt;p>agentgateway doesn&amp;rsquo;t just trace LLM calls — it also traces MCP (Model Context Protocol) tool interactions:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tool discovery&lt;/strong> (&lt;code>tools/list&lt;/code>) — which tools are available, how long discovery takes&lt;/li>
&lt;li>&lt;strong>Tool execution&lt;/strong> (&lt;code>tools/call&lt;/code>) — parameters, results, latency&lt;/li>
&lt;li>&lt;strong>Backend MCP server performance&lt;/strong> — per-server latency and error rates&lt;/li>
&lt;/ul>
&lt;p>When an agent calls Slack, GitHub, or any MCP tool server through agentgateway, the full tool call chain appears in Langfuse alongside the LLM calls that triggered it.&lt;/p>
&lt;hr>
&lt;h2 id="security-policy-visibility">Security Policy Visibility&lt;/h2>
&lt;p>When agentgateway&amp;rsquo;s security policies fire, the trace metadata includes what happened:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>PII Protection&lt;/strong> — how many entities were redacted, what types (email, SSN, phone)&lt;/li>
&lt;li>&lt;strong>Prompt Injection&lt;/strong> — whether an injection was detected and blocked&lt;/li>
&lt;li>&lt;strong>Credential Leak&lt;/strong> — whether secrets were caught in the LLM response&lt;/li>
&lt;li>&lt;strong>Rate Limiting&lt;/strong> — remaining quota for the user&lt;/li>
&lt;/ul>
&lt;p>This means you can see not just &lt;em>what&lt;/em> your agents are doing, but &lt;em>what guardrails are protecting them&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Problem&lt;/th>
&lt;th>Fix&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>No traces in Langfuse&lt;/td>
&lt;td>Check collector pod is running: &lt;code>kubectl get pods -n agentgateway-system -l app=langfuse-otel-collector&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>401 errors in collector logs&lt;/td>
&lt;td>Wrong Langfuse API credentials — re-check the base64 encoding&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Traces show but no prompt/completion&lt;/td>
&lt;td>Add the &lt;code>fields.add&lt;/code> section in Enterprise, or check OTEL env vars in OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Missing gateway metadata&lt;/td>
&lt;td>Restart proxies after config change: &lt;code>kubectl rollout restart deployment -n agentgateway-system&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="full-source-code">Full Source Code&lt;/h2>
&lt;p>All manifests, configs, and examples are in the &lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse">agentgateway-langfuse&lt;/a> repository:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Path&lt;/th>
&lt;th>Description&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/blob/main/docs/quickstart-kind.md">docs/quickstart-kind.md&lt;/a>&lt;/td>
&lt;td>Full kind cluster quickstart guide&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/tree/main/examples/basic">examples/basic/&lt;/a>&lt;/td>
&lt;td>Basic Langfuse collector + tracing config&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/tree/main/examples/fan-out">examples/fan-out/&lt;/a>&lt;/td>
&lt;td>Fan-out to Langfuse + additional backends&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/tree/main/examples/argocd">examples/argocd/&lt;/a>&lt;/td>
&lt;td>Production ArgoCD/GitOps deployment&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;a href="https://github.com/ProfessorSeb/agentgateway-langfuse/blob/main/scripts/verify.sh">scripts/verify.sh&lt;/a>&lt;/td>
&lt;td>End-to-end verification script&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="resources">Resources&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://agentgateway.dev">agentgateway OSS&lt;/a> — CNCF open-source AI gateway&lt;/li>
&lt;li>&lt;a href="https://agentgateway.dev/docs/kubernetes/latest/install/helm/">agentgateway Helm Install&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://langfuse.com">Langfuse&lt;/a> — Open-source LLM observability&lt;/li>
&lt;li>&lt;a href="https://langfuse.com/docs/integrations/opentelemetry">Langfuse OpenTelemetry Docs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry GenAI Conventions&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>Your First AI Route: Connecting to OpenAI with agentgateway</title><link>https://maniak.io/articles/2026-02-11-your-first-ai-route-connecting-to-openai-opensource/</link><pubDate>Wed, 11 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/2026-02-11-your-first-ai-route-connecting-to-openai-opensource/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>This is a how-to guide to setup agentgateway and get your first AI route working with OpenAI. We&amp;rsquo;ll walk through the complete setup from scratch - creating a Kubernetes cluster, installing agentgateway, and connecting it to OpenAI&amp;rsquo;s API.&lt;/p>
&lt;h2 id="what-is-agentgateway">What is agentgateway?&lt;/h2>
&lt;p>agentgateway is an open source, AI-native data plane built in Rust for connecting, securing, and observing AI traffic. Originally created by Solo.io and now a Linux Foundation project, it acts as a purpose-built proxy layer between your applications and AI services like LLMs, MCP tool servers, and other AI agents.&lt;/p>
&lt;ul>
&lt;li>Traditional API gateways don&amp;rsquo;t fit AI workloads — AI inference requests are long-running (minutes vs milliseconds), have larger payloads, and can consume entire GPUs, unlike standard web traffic.&lt;/li>
&lt;li>Connectivity — Unified interface to route requests to LLM providers (OpenAI, Anthropic, Bedrock, etc.), self-hosted models, and MCP tool servers.&lt;/li>
&lt;li>Security — Built-in auth, RBAC, and secrets management for API keys and sensitive data.&lt;/li>
&lt;li>Observability — Automatic token counting, cost tracking, and OpenTelemetry-compatible structured logs.&lt;/li>
&lt;li>MCP support — Can federate multiple MCP servers behind a single endpoint, and expose legacy REST APIs as MCP tools via OpenAPI integration.&lt;/li>
&lt;li>A2A support — Native Agent-to-Agent protocol for secure inter-agent communication..&lt;/li>
&lt;/ul>
&lt;p>In this tutorial, we&amp;rsquo;ll focus on one of agentgateway&amp;rsquo;s most common use cases: routing requests to an LLM provider (OpenAI) with secure credential management and built-in cost observability.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Create a Kubernetes cluster and install agentgateway&lt;/li>
&lt;li>Set up secure OpenAI API key storage&lt;/li>
&lt;li>Configure agentgateway to route to OpenAI&lt;/li>
&lt;li>Test chat completions, embeddings, and model listings&lt;/li>
&lt;li>Monitor real AI requests and track costs&lt;/li>
&lt;li>Troubleshoot common issues&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Docker installed and running&lt;/li>
&lt;li>kubectl CLI tool&lt;/li>
&lt;li>Helm 3.x installed&lt;/li>
&lt;li>Valid OpenAI API Key with credits (get from &lt;a href="https://platform.openai.com">OpenAI Platform&lt;/a>)&lt;/li>
&lt;li>Basic understanding of Kubernetes and OpenAI API structure&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="step-1-environment-setup">Step 1: Environment Setup&lt;/h2>
&lt;p>In this step, we&amp;rsquo;ll create a local Kubernetes cluster using kind (Kubernetes in Docker) and install agentgateway. This gives us a complete testing environment that mirrors production setups but runs entirely on your local machine.&lt;/p>
&lt;h3 id="install-kind">Install Kind&lt;/h3>
&lt;p>Kind creates Kubernetes clusters using Docker containers as nodes. This is perfect for development and testing because it&amp;rsquo;s lightweight, fast to spin up, and doesn&amp;rsquo;t require cloud resources.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew install kind
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># On Linux&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x ./kind &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> sudo mv ./kind /usr/local/bin/kind
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-kind-cluster">Create Kind Cluster&lt;/h3>
&lt;p>This creates a single-node Kubernetes cluster that will host our agentgateway installation. The cluster provides the foundation for all the networking, security, and routing capabilities we&amp;rsquo;ll configure.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify cluster is ready&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-agentgateway">Install agentgateway&lt;/h3>
&lt;p>agentgateway installation happens in three phases: First we install the Kubernetes Gateway API (the standard for ingress traffic), then agentgateway&amp;rsquo;s custom resources, and finally the control plane that manages everything. This separation allows for better modularity and easier upgrades.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Install Gateway API CRDs (version 1.4.0)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Install agentgateway CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0 agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Install agentgateway control plane&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i -n agentgateway-system agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. Verify installation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-2-openai-api-key-setup">Step 2: OpenAI API Key Setup&lt;/h2>
&lt;p>Security is paramount when working with AI services. Instead of embedding API keys directly in configurations, we&amp;rsquo;ll use Kubernetes secrets to store credentials securely. This approach ensures keys are encrypted at rest and can be rotated without changing application code.&lt;/p>
&lt;h3 id="get-your-openai-api-key">Get Your OpenAI API Key&lt;/h3>
&lt;p>OpenAI uses API keys for authentication and billing. Each key is tied to your account and usage limits, making it essential to secure them properly.&lt;/p>
&lt;ol>
&lt;li>Visit &lt;a href="https://platform.openai.com">OpenAI Platform&lt;/a>&lt;/li>
&lt;li>Navigate to API Keys section and create a new key&lt;/li>
&lt;li>Set usage limits to control costs&lt;/li>
&lt;li>Copy your API key securely&lt;/li>
&lt;/ol>
&lt;h3 id="test-your-api-key">Test Your API Key&lt;/h3>
&lt;p>Before integrating with agentgateway, we&amp;rsquo;ll verify the API key works directly with OpenAI&amp;rsquo;s API. This eliminates the key as a potential issue if something goes wrong later in the setup.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set your OpenAI API key (replace with your actual key)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-your-openai-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test the key directly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0:3] | .[].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-kubernetes-secret">Create Kubernetes Secret&lt;/h3>
&lt;p>Kubernetes secrets provide a secure way to store sensitive data like API keys. We format the key as a complete Authorization header (&lt;code>Bearer sk-...&lt;/code>) so agentgateway can use it directly without modification. The &lt;code>--dry-run=client -o yaml | kubectl apply -f -&lt;/code> pattern ensures the secret is created safely even if it already exists.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create secret with proper authorization header format&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-secret &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Authorization=Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dry-run&lt;span class="o">=&lt;/span>client -o yaml &lt;span class="p">|&lt;/span> kubectl apply -f -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret creation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-3-configure-agentgateway">Step 3: Configure agentgateway&lt;/h2>
&lt;p>Now we&amp;rsquo;ll configure the core components that make AI routing work. agentgateway follows the Kubernetes Gateway API pattern with three main resources: Gateway (the entry point), Backends (destination services), and HTTPRoutes (traffic routing rules). This declarative approach makes configurations version-controllable and environment-portable.&lt;/p>
&lt;h3 id="create-gateway-resource">Create Gateway Resource&lt;/h3>
&lt;p>The Gateway resource defines the entry point for all incoming traffic. It specifies which ports to listen on, what protocols to accept, and which namespaces can create routes through it. Think of it as the front door to your AI services.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> allowedRoutes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespaces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from: All
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-openai-backend">Create OpenAI Backend&lt;/h3>
&lt;p>AgentgatewayBackend resources define how to connect to AI services. The &lt;code>ai.provider.openai&lt;/code> section tells agentgateway this is an AI service that expects OpenAI-compatible requests. The authentication policy references our secret, and the timeout ensures long-running AI requests don&amp;rsquo;t hang indefinitely.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-http-routes">Create HTTP Routes&lt;/h3>
&lt;p>Create an HTTPRoute resource that routes incoming traffic to the AgentgatewayBackend. The following example sets up a route. Note that agentgateway automatically rewrites the endpoint to the OpenAI /v1/chat/completions endpoint.&lt;/p>
&lt;p>Create the HTTP routes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-configuration">Verify Configuration&lt;/h3>
&lt;p>Before testing, we&amp;rsquo;ll check that all our resources are properly created and accepted by the agentgateway controller. The &lt;code>Accepted&lt;/code> status indicates that configurations are valid and the controller can proceed with implementation.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check both backends&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check routes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-4-testing-your-setup">Step 4: Testing Your Setup&lt;/h2>
&lt;p>With our configuration complete, it&amp;rsquo;s time to test the AI routing. Since we&amp;rsquo;re using a local kind cluster, we&amp;rsquo;ll use port-forwarding to access the Gateway service. In production, this would be handled by a LoadBalancer or Ingress controller.&lt;/p>
&lt;h3 id="setup-port-forward">Setup Port-Forward&lt;/h3>
&lt;p>Port-forwarding creates a tunnel from your local machine to the agentgateway service inside the Kubernetes cluster. This lets us test the setup without exposing services publicly.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Port-forward agentgateway service in background&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-chat-completions">Test Chat Completions&lt;/h3>
&lt;p>This is where the magic happens! Our request travels through agentgateway, gets authenticated using our secret, routed to OpenAI&amp;rsquo;s API, and returns with a complete AI response. Notice how the response includes token usage information that agentgateway automatically captures for cost tracking and observability.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test basic chat completion&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;localhost:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What are the key benefits of using an AI Gateway?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Expected Response:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chatcmpl-abc123def456&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chat.completion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1701234567&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;gpt-4o-mini-2024-07-18&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;choices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;An AI Gateway provides unified access to multiple AI providers, centralized security and authentication, comprehensive observability and cost tracking, rate limiting and quotas, and improved reliability through failover and retry mechanisms.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;finish_reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;stop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;usage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;prompt_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">15&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;completion_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">35&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;total_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-different-models">Test Different Models&lt;/h3>
&lt;p>agentgateway allows you to easily switch between different OpenAI models by simply changing the &lt;code>model&lt;/code> parameter. The backend automatically routes to the appropriate model while maintaining consistent authentication and observability.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with GPT-4o&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;localhost:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Explain agentgateway in one sentence.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.choices[0].message.content&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>When working with distributed systems like Kubernetes and external APIs, issues can arise at multiple layers. This section covers the most common problems you might encounter and how to systematically diagnose them. The key is to test each layer independently: network connectivity, authentication, resource configuration, and API compatibility.&lt;/p>
&lt;h3 id="common-issues">Common Issues&lt;/h3>
&lt;p>&lt;strong>1. Service Not Found Error:&lt;/strong>
This usually means the service name doesn&amp;rsquo;t match what was actually created during installation. Different agentgateway versions or installation methods may create services with different names.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check what services exist&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If agentgateway-proxy doesn&amp;#39;t exist, use the correct service name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n agentgateway-system &lt;span class="p">|&lt;/span> grep -i gateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. Authentication Errors (401):&lt;/strong>
Authentication failures typically indicate either an invalid API key or incorrect secret formatting. Always test the key directly with OpenAI before troubleshooting agentgateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n agentgateway-system -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test API key directly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. Routes Not Working:&lt;/strong>
Route issues often stem from mismatched resource names or namespaces. The Gateway, HTTPRoute, and Backend must all reference each other correctly for traffic to flow.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check backend status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe agentgatewaybackend openai-backend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check route status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe httproute openai-chat -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Gateway status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>4. Port-Forward Issues:&lt;/strong>
Port conflicts are common on development machines. If port 8080 is busy, either stop the conflicting service or use a different port.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if port 8080 is in use&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lsof -i :8080
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Try a different port&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8081:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;8081&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="debug-commands">Debug Commands&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># View all agentgateway resources&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend,gateway,httproute -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check pod logs for errors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n agentgateway-system --tail&lt;span class="o">=&lt;/span>&lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test connectivity from inside cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n agentgateway-system deploy/agentgateway -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -v https://api.openai.com/v1/models &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done experimenting, it&amp;rsquo;s important to clean up resources to free up system resources and avoid any potential costs. The cleanup process should happen in reverse order: stop network connections first, then remove application resources, and finally remove infrastructure.&lt;/p>
&lt;h3 id="stop-port-forward">Stop Port-Forward&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Kill the port-forward process&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">kill&lt;/span> &lt;span class="nv">$PORTFORWARD_PID&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="remove-resources-optional">Remove Resources (Optional)&lt;/h3>
&lt;p>This removes all the agentgateway configuration we created, but leaves the agentgateway installation intact for future experiments. Remove resources in dependency order: routes first (they reference backends), then backends, then the Gateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove all OpenAI configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute openai-chat openai-models -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend openai-backend openai-models-backend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret openai-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="remove-kind-cluster-optional">Remove Kind Cluster (Optional)&lt;/h3>
&lt;p>This completely removes the Kubernetes cluster and all associated resources. Only do this if you&amp;rsquo;re completely done with the tutorial, as you&amp;rsquo;ll need to recreate everything from Step 1 to run it again.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the entire cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>Now that you have a working AI gateway, you can build on this foundation to create production-ready AI infrastructure:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Add more providers&lt;/strong> - Configure Anthropic, AWS Bedrock, or Azure OpenAI for multi-provider setups and failover scenarios&lt;/li>
&lt;li>&lt;strong>Implement security&lt;/strong> - Add rate limiting, authentication, and guardrails to protect against abuse and unexpected costs&lt;/li>
&lt;li>&lt;strong>Set up monitoring&lt;/strong> - Configure Grafana dashboards and alerting to track performance, costs, and usage patterns across teams&lt;/li>
&lt;li>&lt;strong>Explore advanced routing&lt;/strong> - Implement path-based, header-based, and weighted routing to direct different types of requests to optimal models&lt;/li>
&lt;/ul>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;p>This tutorial demonstrates several important concepts for production AI systems:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>agentgateway provides a unified interface&lt;/strong> to AI providers with minimal overhead, making it easy to switch providers or implement failover&lt;/li>
&lt;li>&lt;strong>Proper secret management&lt;/strong> is essential for production deployments - never embed API keys in code or configuration files&lt;/li>
&lt;li>&lt;strong>Built-in observability&lt;/strong> gives immediate insights into costs and performance without requiring additional tooling or instrumentation&lt;/li>
&lt;li>&lt;strong>The Gateway API pattern&lt;/strong> makes routing configuration declarative and portable across different Kubernetes environments&lt;/li>
&lt;li>&lt;strong>Dual backend types&lt;/strong> (AI-aware vs static HTTP) allow you to handle both complex AI workloads and simple metadata requests efficiently&lt;/li>
&lt;li>&lt;strong>Kind clusters&lt;/strong> are perfect for local development and testing, providing a production-like environment without cloud costs&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway is now successfully routing requests to OpenAI with enterprise-grade security, observability, and cost control! You&amp;rsquo;ve built a foundation that can scale from development to production while maintaining visibility and control over your AI infrastructure. 🎯&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>This is a how-to guide to setup agentgateway and get your first AI route working with OpenAI. We&amp;rsquo;ll walk through the complete setup from scratch - creating a Kubernetes cluster, installing agentgateway, and connecting it to OpenAI&amp;rsquo;s API.&lt;/p>
&lt;h2 id="what-is-agentgateway">What is agentgateway?&lt;/h2>
&lt;p>agentgateway is an open source, AI-native data plane built in Rust for connecting, securing, and observing AI traffic. Originally created by Solo.io and now a Linux Foundation project, it acts as a purpose-built proxy layer between your applications and AI services like LLMs, MCP tool servers, and other AI agents.&lt;/p>
&lt;ul>
&lt;li>Traditional API gateways don&amp;rsquo;t fit AI workloads — AI inference requests are long-running (minutes vs milliseconds), have larger payloads, and can consume entire GPUs, unlike standard web traffic.&lt;/li>
&lt;li>Connectivity — Unified interface to route requests to LLM providers (OpenAI, Anthropic, Bedrock, etc.), self-hosted models, and MCP tool servers.&lt;/li>
&lt;li>Security — Built-in auth, RBAC, and secrets management for API keys and sensitive data.&lt;/li>
&lt;li>Observability — Automatic token counting, cost tracking, and OpenTelemetry-compatible structured logs.&lt;/li>
&lt;li>MCP support — Can federate multiple MCP servers behind a single endpoint, and expose legacy REST APIs as MCP tools via OpenAPI integration.&lt;/li>
&lt;li>A2A support — Native Agent-to-Agent protocol for secure inter-agent communication..&lt;/li>
&lt;/ul>
&lt;p>In this tutorial, we&amp;rsquo;ll focus on one of agentgateway&amp;rsquo;s most common use cases: routing requests to an LLM provider (OpenAI) with secure credential management and built-in cost observability.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Create a Kubernetes cluster and install agentgateway&lt;/li>
&lt;li>Set up secure OpenAI API key storage&lt;/li>
&lt;li>Configure agentgateway to route to OpenAI&lt;/li>
&lt;li>Test chat completions, embeddings, and model listings&lt;/li>
&lt;li>Monitor real AI requests and track costs&lt;/li>
&lt;li>Troubleshoot common issues&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Docker installed and running&lt;/li>
&lt;li>kubectl CLI tool&lt;/li>
&lt;li>Helm 3.x installed&lt;/li>
&lt;li>Valid OpenAI API Key with credits (get from &lt;a href="https://platform.openai.com">OpenAI Platform&lt;/a>)&lt;/li>
&lt;li>Basic understanding of Kubernetes and OpenAI API structure&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="step-1-environment-setup">Step 1: Environment Setup&lt;/h2>
&lt;p>In this step, we&amp;rsquo;ll create a local Kubernetes cluster using kind (Kubernetes in Docker) and install agentgateway. This gives us a complete testing environment that mirrors production setups but runs entirely on your local machine.&lt;/p>
&lt;h3 id="install-kind">Install Kind&lt;/h3>
&lt;p>Kind creates Kubernetes clusters using Docker containers as nodes. This is perfect for development and testing because it&amp;rsquo;s lightweight, fast to spin up, and doesn&amp;rsquo;t require cloud resources.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew install kind
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># On Linux&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x ./kind &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> sudo mv ./kind /usr/local/bin/kind
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-kind-cluster">Create Kind Cluster&lt;/h3>
&lt;p>This creates a single-node Kubernetes cluster that will host our agentgateway installation. The cluster provides the foundation for all the networking, security, and routing capabilities we&amp;rsquo;ll configure.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind create cluster --name agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify cluster is ready&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-agentgateway">Install agentgateway&lt;/h3>
&lt;p>agentgateway installation happens in three phases: First we install the Kubernetes Gateway API (the standard for ingress traffic), then agentgateway&amp;rsquo;s custom resources, and finally the control plane that manages everything. This separation allows for better modularity and easier upgrades.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Install Gateway API CRDs (version 1.4.0)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Install agentgateway CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0 agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Install agentgateway control plane&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i -n agentgateway-system agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://ghcr.io/kgateway-dev/charts/agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version v2.2.0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. Verify installation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-2-openai-api-key-setup">Step 2: OpenAI API Key Setup&lt;/h2>
&lt;p>Security is paramount when working with AI services. Instead of embedding API keys directly in configurations, we&amp;rsquo;ll use Kubernetes secrets to store credentials securely. This approach ensures keys are encrypted at rest and can be rotated without changing application code.&lt;/p>
&lt;h3 id="get-your-openai-api-key">Get Your OpenAI API Key&lt;/h3>
&lt;p>OpenAI uses API keys for authentication and billing. Each key is tied to your account and usage limits, making it essential to secure them properly.&lt;/p>
&lt;ol>
&lt;li>Visit &lt;a href="https://platform.openai.com">OpenAI Platform&lt;/a>&lt;/li>
&lt;li>Navigate to API Keys section and create a new key&lt;/li>
&lt;li>Set usage limits to control costs&lt;/li>
&lt;li>Copy your API key securely&lt;/li>
&lt;/ol>
&lt;h3 id="test-your-api-key">Test Your API Key&lt;/h3>
&lt;p>Before integrating with agentgateway, we&amp;rsquo;ll verify the API key works directly with OpenAI&amp;rsquo;s API. This eliminates the key as a potential issue if something goes wrong later in the setup.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set your OpenAI API key (replace with your actual key)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-your-openai-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test the key directly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0:3] | .[].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-kubernetes-secret">Create Kubernetes Secret&lt;/h3>
&lt;p>Kubernetes secrets provide a secure way to store sensitive data like API keys. We format the key as a complete Authorization header (&lt;code>Bearer sk-...&lt;/code>) so agentgateway can use it directly without modification. The &lt;code>--dry-run=client -o yaml | kubectl apply -f -&lt;/code> pattern ensures the secret is created safely even if it already exists.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create secret with proper authorization header format&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-secret &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n agentgateway-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Authorization=Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dry-run&lt;span class="o">=&lt;/span>client -o yaml &lt;span class="p">|&lt;/span> kubectl apply -f -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret creation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-3-configure-agentgateway">Step 3: Configure agentgateway&lt;/h2>
&lt;p>Now we&amp;rsquo;ll configure the core components that make AI routing work. agentgateway follows the Kubernetes Gateway API pattern with three main resources: Gateway (the entry point), Backends (destination services), and HTTPRoutes (traffic routing rules). This declarative approach makes configurations version-controllable and environment-portable.&lt;/p>
&lt;h3 id="create-gateway-resource">Create Gateway Resource&lt;/h3>
&lt;p>The Gateway resource defines the entry point for all incoming traffic. It specifies which ports to listen on, what protocols to accept, and which namespaces can create routes through it. Think of it as the front door to your AI services.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> allowedRoutes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespaces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from: All
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-openai-backend">Create OpenAI Backend&lt;/h3>
&lt;p>AgentgatewayBackend resources define how to connect to AI services. The &lt;code>ai.provider.openai&lt;/code> section tells agentgateway this is an AI service that expects OpenAI-compatible requests. The authentication policy references our secret, and the timeout ensures long-running AI requests don&amp;rsquo;t hang indefinitely.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-http-routes">Create HTTP Routes&lt;/h3>
&lt;p>Create an HTTPRoute resource that routes incoming traffic to the AgentgatewayBackend. The following example sets up a route. Note that agentgateway automatically rewrites the endpoint to the OpenAI /v1/chat/completions endpoint.&lt;/p>
&lt;p>Create the HTTP routes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway-proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: agentgateway-system
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-configuration">Verify Configuration&lt;/h3>
&lt;p>Before testing, we&amp;rsquo;ll check that all our resources are properly created and accepted by the agentgateway controller. The &lt;code>Accepted&lt;/code> status indicates that configurations are valid and the controller can proceed with implementation.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check both backends&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check routes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Gateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="step-4-testing-your-setup">Step 4: Testing Your Setup&lt;/h2>
&lt;p>With our configuration complete, it&amp;rsquo;s time to test the AI routing. Since we&amp;rsquo;re using a local kind cluster, we&amp;rsquo;ll use port-forwarding to access the Gateway service. In production, this would be handled by a LoadBalancer or Ingress controller.&lt;/p>
&lt;h3 id="setup-port-forward">Setup Port-Forward&lt;/h3>
&lt;p>Port-forwarding creates a tunnel from your local machine to the agentgateway service inside the Kubernetes cluster. This lets us test the setup without exposing services publicly.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Port-forward agentgateway service in background&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8080:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-chat-completions">Test Chat Completions&lt;/h3>
&lt;p>This is where the magic happens! Our request travels through agentgateway, gets authenticated using our secret, routed to OpenAI&amp;rsquo;s API, and returns with a complete AI response. Notice how the response includes token usage information that agentgateway automatically captures for cost tracking and observability.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test basic chat completion&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;localhost:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What are the key benefits of using an AI Gateway?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Expected Response:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chatcmpl-abc123def456&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chat.completion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1701234567&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;gpt-4o-mini-2024-07-18&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;choices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;An AI Gateway provides unified access to multiple AI providers, centralized security and authentication, comprehensive observability and cost tracking, rate limiting and quotas, and improved reliability through failover and retry mechanisms.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;finish_reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;stop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;usage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;prompt_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">15&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;completion_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">35&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;total_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-different-models">Test Different Models&lt;/h3>
&lt;p>agentgateway allows you to easily switch between different OpenAI models by simply changing the &lt;code>model&lt;/code> parameter. The backend automatically routes to the appropriate model while maintaining consistent authentication and observability.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with GPT-4o&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;localhost:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Explain agentgateway in one sentence.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.choices[0].message.content&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;p>When working with distributed systems like Kubernetes and external APIs, issues can arise at multiple layers. This section covers the most common problems you might encounter and how to systematically diagnose them. The key is to test each layer independently: network connectivity, authentication, resource configuration, and API compatibility.&lt;/p>
&lt;h3 id="common-issues">Common Issues&lt;/h3>
&lt;p>&lt;strong>1. Service Not Found Error:&lt;/strong>
This usually means the service name doesn&amp;rsquo;t match what was actually created during installation. Different agentgateway versions or installation methods may create services with different names.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check what services exist&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If agentgateway-proxy doesn&amp;#39;t exist, use the correct service name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n agentgateway-system &lt;span class="p">|&lt;/span> grep -i gateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. Authentication Errors (401):&lt;/strong>
Authentication failures typically indicate either an invalid API key or incorrect secret formatting. Always test the key directly with OpenAI before troubleshooting agentgateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret exists&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n agentgateway-system -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test API key directly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. Routes Not Working:&lt;/strong>
Route issues often stem from mismatched resource names or namespaces. The Gateway, HTTPRoute, and Backend must all reference each other correctly for traffic to flow.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check backend status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe agentgatewaybackend openai-backend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check route status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe httproute openai-chat -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Gateway status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>4. Port-Forward Issues:&lt;/strong>
Port conflicts are common on development machines. If port 8080 is busy, either stop the conflicting service or use a different port.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check if port 8080 is in use&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lsof -i :8080
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Try a different port&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl port-forward -n agentgateway-system svc/agentgateway-proxy 8081:8080 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;8081&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="debug-commands">Debug Commands&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># View all agentgateway resources&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend,gateway,httproute -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check pod logs for errors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n agentgateway-system --tail&lt;span class="o">=&lt;/span>&lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test connectivity from inside cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n agentgateway-system deploy/agentgateway -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -v https://api.openai.com/v1/models &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done experimenting, it&amp;rsquo;s important to clean up resources to free up system resources and avoid any potential costs. The cleanup process should happen in reverse order: stop network connections first, then remove application resources, and finally remove infrastructure.&lt;/p>
&lt;h3 id="stop-port-forward">Stop Port-Forward&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Kill the port-forward process&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">kill&lt;/span> &lt;span class="nv">$PORTFORWARD_PID&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="remove-resources-optional">Remove Resources (Optional)&lt;/h3>
&lt;p>This removes all the agentgateway configuration we created, but leaves the agentgateway installation intact for future experiments. Remove resources in dependency order: routes first (they reference backends), then backends, then the Gateway.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove all OpenAI configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute openai-chat openai-models -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend openai-backend openai-models-backend -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete gateway agentgateway-proxy -n agentgateway-system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete secret openai-secret -n agentgateway-system
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="remove-kind-cluster-optional">Remove Kind Cluster (Optional)&lt;/h3>
&lt;p>This completely removes the Kubernetes cluster and all associated resources. Only do this if you&amp;rsquo;re completely done with the tutorial, as you&amp;rsquo;ll need to recreate everything from Step 1 to run it again.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the entire cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>Now that you have a working AI gateway, you can build on this foundation to create production-ready AI infrastructure:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Add more providers&lt;/strong> - Configure Anthropic, AWS Bedrock, or Azure OpenAI for multi-provider setups and failover scenarios&lt;/li>
&lt;li>&lt;strong>Implement security&lt;/strong> - Add rate limiting, authentication, and guardrails to protect against abuse and unexpected costs&lt;/li>
&lt;li>&lt;strong>Set up monitoring&lt;/strong> - Configure Grafana dashboards and alerting to track performance, costs, and usage patterns across teams&lt;/li>
&lt;li>&lt;strong>Explore advanced routing&lt;/strong> - Implement path-based, header-based, and weighted routing to direct different types of requests to optimal models&lt;/li>
&lt;/ul>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;p>This tutorial demonstrates several important concepts for production AI systems:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>agentgateway provides a unified interface&lt;/strong> to AI providers with minimal overhead, making it easy to switch providers or implement failover&lt;/li>
&lt;li>&lt;strong>Proper secret management&lt;/strong> is essential for production deployments - never embed API keys in code or configuration files&lt;/li>
&lt;li>&lt;strong>Built-in observability&lt;/strong> gives immediate insights into costs and performance without requiring additional tooling or instrumentation&lt;/li>
&lt;li>&lt;strong>The Gateway API pattern&lt;/strong> makes routing configuration declarative and portable across different Kubernetes environments&lt;/li>
&lt;li>&lt;strong>Dual backend types&lt;/strong> (AI-aware vs static HTTP) allow you to handle both complex AI workloads and simple metadata requests efficiently&lt;/li>
&lt;li>&lt;strong>Kind clusters&lt;/strong> are perfect for local development and testing, providing a production-like environment without cloud costs&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway is now successfully routing requests to OpenAI with enterprise-grade security, observability, and cost control! You&amp;rsquo;ve built a foundation that can scale from development to production while maintaining visibility and control over your AI infrastructure. 🎯&lt;/p></content:encoded></item><item><title>Advanced Routing Patterns for AI Models with agentgateway</title><link>https://maniak.io/articles/05-advanced-routing-patterns-for-ai-models/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/05-advanced-routing-patterns-for-ai-models/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>One of the most powerful features of agentgateway is its ability to intelligently route requests to different AI models or providers based on various criteria. This enables sophisticated scenarios like model selection based on request characteristics, A/B testing different models, and cost optimization through intelligent routing.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll explore multiple routing patterns that transform your agentgateway from a simple proxy into an intelligent AI traffic management system.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Path-based routing for different models and use cases&lt;/li>
&lt;li>Header-based routing for tenant isolation and testing&lt;/li>
&lt;li>Query parameter routing for flexible client control&lt;/li>
&lt;li>Weighted routing for A/B testing and gradual rollouts&lt;/li>
&lt;li>Content-based routing using request body analysis&lt;/li>
&lt;li>Fallback and failover routing strategies&lt;/li>
&lt;li>Cost-optimized routing patterns&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Completed previous blog posts (agentgateway setup, observability, OpenAI integration)&lt;/li>
&lt;li>Understanding of Kubernetes Gateway API concepts&lt;/li>
&lt;li>Knowledge of HTTP routing principles&lt;/li>
&lt;li>OpenAI API key for testing (we&amp;rsquo;ll add Anthropic optionally)&lt;/li>
&lt;/ul>
&lt;h2 id="environment-setup">Environment Setup&lt;/h2>
&lt;p>Ensure your environment is ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify environment variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-your-openai-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-license-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify agentgateway is running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify existing OpenAI configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-1-path-based-routing">Pattern 1: Path-Based Routing&lt;/h2>
&lt;h3 id="model-specific-paths">Model-Specific Paths&lt;/h3>
&lt;p>Create different paths for different models, allowing clients to choose the right model for their use case:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># GPT-4o Mini route (fast, cost-effective)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> use-case: general
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/fast
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;60s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Add model override header
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Use-Case
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: fast-general
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># GPT-4o route (premium quality)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: gpt-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> use-case: premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Use-Case
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: premium-quality
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Embeddings route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-embeddings-route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Service-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-model-specific-backends">Create Model-Specific Backends&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o-mini&amp;#34; # Force specific model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;60s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o&amp;#34; # Force premium model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-path-based-routing">Test Path-Based Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">localhost&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">8080&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test fast/general route (gpt-4o-mini)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Fast Route (GPT-4o Mini) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/fast/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What model are you and what are your strengths?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> usage: .usage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Premium Route (GPT-4o) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/premium/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What model are you and what are your strengths?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> usage: .usage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-2-header-based-routing">Pattern 2: Header-Based Routing&lt;/h2>
&lt;h3 id="tenant-isolation-and-testing">Tenant Isolation and Testing&lt;/h3>
&lt;p>Use headers to route requests to different models or providers based on client characteristics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Development environment route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-dev-environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> environment: development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini # Use cheaper model for dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Tier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: economy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Production environment route 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-prod-environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> environment: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: prod
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o # Use premium model for prod
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Tier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># A/B Testing route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-ab-test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> purpose: ab-testing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Variant A - GPT-4o Mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-a
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Variant
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-a
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Variant B - GPT-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Variant
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-header-based-routing">Test Header-Based Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test development environment routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Development Environment ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-Environment: development&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from development!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Production Environment ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-Environment: production&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from production!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing A/B Test Variant A ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-AB-Test: variant-a&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;A/B test message&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 30
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;(X-AB-Variant|X-Model-Used)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing A/B Test Variant B ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-AB-Test: variant-b&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;A/B test message&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 30
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;(X-AB-Variant|X-Model-Used)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-3-query-parameter-routing">Pattern 3: Query Parameter Routing&lt;/h2>
&lt;h3 id="flexible-client-side-model-selection">Flexible Client-Side Model Selection&lt;/h3>
&lt;p>Allow clients to specify routing preferences via query parameters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-query-param-routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: query-parameter
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Route for speed preference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/flexible
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> queryParams:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: speed
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: fast
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Speed-Optimized
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Route for quality preference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/flexible
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> queryParams:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: quality
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Quality-Optimized
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Route for cost preference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/flexible
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> queryParams:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: minimal
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Cost-Optimized
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-query-parameter-routing">Test Query Parameter Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test speed-optimized routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Speed-Optimized Routing ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/flexible/completions?speed=fast&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Fast response needed!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test quality-optimized routing &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Quality-Optimized Routing ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/flexible/completions?quality=premium&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;High quality response needed!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test cost-optimized routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Cost-Optimized Routing ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/flexible/completions?cost=minimal&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Cost-effective response needed!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-4-weighted-routing-for-gradual-rollouts">Pattern 4: Weighted Routing for Gradual Rollouts&lt;/h2>
&lt;h3 id="implement-traffic-splitting">Implement Traffic Splitting&lt;/h3>
&lt;p>Use multiple backends with different weights for gradual model rollouts:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-weighted-routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: weighted
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/gradual-rollout
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Split traffic between models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # 80% traffic to stable model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # 20% traffic to new model for testing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Routing-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: weighted-rollout
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-weighted-routing">Test Weighted Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; test-weighted-routing.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_PORT=&amp;#34;${GATEWAY_PORT:-8080}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">REQUESTS=${1:-20}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Testing weighted routing with $REQUESTS requests...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Expected: ~80% gpt-4o-mini, ~20% gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">declare -A model_counts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for ((i=1; i&amp;lt;=REQUESTS; i++)); do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response=$(curl -s &amp;#34;$GATEWAY_IP:$GATEWAY_PORT/ai/gradual-rollout/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;What model are you?&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;max_tokens&amp;#34;: 10
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model=$(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.model&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ((model_counts[$model]++))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;Request %2d: %s\n&amp;#34; $i &amp;#34;$model&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;=== Results ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for model in &amp;#34;${!model_counts[@]}&amp;#34;; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> count=${model_counts[$model]}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> percentage=$(( count * 100 / REQUESTS ))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;$model: $count requests (${percentage}%)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x test-weighted-routing.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test-weighted-routing.sh &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-5-content-based-routing">Pattern 5: Content-Based Routing&lt;/h2>
&lt;h3 id="route-based-on-request-content">Route Based on Request Content&lt;/h3>
&lt;p>Use agentgateway&amp;rsquo;s request analysis capabilities to route based on content characteristics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: enterpriseagentgateway.solo.io/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: EnterpriseAgentgatewayPolicy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: content-based-routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - group: gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> traffic:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Simple content analysis - route long requests to premium model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> requestTransformation:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> inline:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Add request length as header for routing decisions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> X-Content-Length: &amp;#39;string(len(json(request.body).messages[0].content))&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> X-Content-Type: &amp;#39;if len(json(request.body).messages[0].content) &amp;gt; 100 then &amp;#34;long&amp;#34; else &amp;#34;short&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Route short content to fast model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-short-content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: content-based
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> content-type: short
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/smart
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: short
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: short-content-fast-model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Route long content to premium model 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-long-content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: content-based
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> content-type: long
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/smart
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: long
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: long-content-premium-model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-content-based-routing">Test Content-Based Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with short content&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Short Content (should use gpt-4o-mini) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/smart/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hi!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Long Content (should use gpt-4o) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/smart/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;This is a much longer prompt that contains significantly more content and should trigger the routing logic to use the premium model because it requires more sophisticated processing and understanding. The content-based routing should detect this as a long request and route it to GPT-4o for better handling of complex queries that require more nuanced responses and deeper analysis.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="observing-routing-patterns">Observing Routing Patterns&lt;/h2>
&lt;h3 id="create-routing-dashboard">Create Routing Dashboard&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; routing-analysis.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Analyzing routing patterns from agentgateway logs...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Extract routing information from recent logs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail=100 | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq -r &amp;#39;select(.gen_ai) | [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .timestamp,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .route_name // &amp;#34;unknown&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.request.model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> (.gen_ai.usage.total_tokens // 0),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> (.request.headers[&amp;#34;x-environment&amp;#34;] // &amp;#34;none&amp;#34;),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> (.request.headers[&amp;#34;x-ab-test&amp;#34;] // &amp;#34;none&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ] | @csv&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> awk -F&amp;#39;,&amp;#39; &amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">BEGIN {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Route Analysis Report&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;====================&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_requests = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> split(&amp;#34;&amp;#34;, route_counts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> split(&amp;#34;&amp;#34;, model_counts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> split(&amp;#34;&amp;#34;, env_counts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_requests++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Remove quotes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> route = $2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model = $3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tokens = $4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> env = $5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, route)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, model) 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, env)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> route_counts[route]++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model_counts[model]++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (env != &amp;#34;none&amp;#34;) env_counts[env]++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_tokens += tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">END {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Total Requests: &amp;#34; total_requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Total Tokens: &amp;#34; total_tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Routes:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for (route in route_counts) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34; %s: %d requests\n&amp;#34;, route, route_counts[route]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Models:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for (model in model_counts) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34; %s: %d requests\n&amp;#34;, model, model_counts[model]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Environments:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for (env in env_counts) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34; %s: %d requests\n&amp;#34;, env, env_counts[env]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x routing-analysis.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./routing-analysis.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="monitor-routing-in-grafana">Monitor Routing in Grafana&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Open Grafana Dashboard&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n monitoring svc/grafana-prometheus 3000:3000 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Create Custom Routing Queries&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Request rate by route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">agentgateway_requests_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">route_name&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Request rate by model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">agentgateway_requests_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">model&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Token usage by routing pattern&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">agentgateway_tokens_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">route_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">token_type&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Watch routing patterns&lt;/strong> as you send test requests through different patterns&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="pattern-6-fallback-and-failover-routing">Pattern 6: Fallback and Failover Routing&lt;/h2>
&lt;h3 id="primary-secondary-backend-configuration">Primary-Secondary Backend Configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Primary backend with health checking
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-primary
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Health checking configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> healthCheck:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> interval: &amp;#34;30s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout: &amp;#34;5s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> unhealthyThreshold: 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> healthyThreshold: 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Fallback backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-fallback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Failover route configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-failover
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: failover
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/reliable
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Primary backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-primary
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Fallback backend (only used if primary fails)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-fallback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Routing-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: failover-enabled
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="creating-routing-test-suite">Creating Routing Test Suite&lt;/h2>
&lt;h3 id="comprehensive-routing-test">Comprehensive Routing Test&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; comprehensive-routing-test.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_PORT=&amp;#34;${GATEWAY_PORT:-8080}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Comprehensive Routing Pattern Test Suite&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;=======================================&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local name=&amp;#34;$1&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local endpoint=&amp;#34;$2&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local headers=&amp;#34;$3&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local expected_model=&amp;#34;$4&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Testing: $name&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Endpoint: $endpoint&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local response=$(curl -s $headers &amp;#34;$GATEWAY_IP:$GATEWAY_PORT$endpoint&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;What model are you?&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;max_tokens&amp;#34;: 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local actual_model=$(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.model&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local tokens=$(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.usage.total_tokens&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [[ &amp;#34;$actual_model&amp;#34; == *&amp;#34;$expected_model&amp;#34;* ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✓ Success: $actual_model ($tokens tokens)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✗ Failed: Expected $expected_model, got $actual_model&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;1. Path-based routing tests&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;---------------------------&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Fast Route&amp;#34; &amp;#34;/ai/fast/completions&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Premium Route&amp;#34; &amp;#34;/ai/premium/completions&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;2. Header-based routing tests&amp;#34; 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;-----------------------------&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Dev Environment&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-Environment: development&amp;#39;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Prod Environment&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-Environment: production&amp;#39;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;A/B Test Variant A&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-AB-Test: variant-a&amp;#39;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;A/B Test Variant B&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-AB-Test: variant-b&amp;#39;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;3. Query parameter routing tests&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;--------------------------------&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Speed Optimized&amp;#34; &amp;#34;/ai/flexible/completions?speed=fast&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Quality Optimized&amp;#34; &amp;#34;/ai/flexible/completions?quality=premium&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Cost Optimized&amp;#34; &amp;#34;/ai/flexible/completions?cost=minimal&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Routing test suite complete!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Check Grafana dashboard to see routing patterns and metrics.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x comprehensive-routing-test.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./comprehensive-routing-test.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="best-practices-and-considerations">Best Practices and Considerations&lt;/h2>
&lt;h3 id="routing-strategy-guidelines">Routing Strategy Guidelines&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Performance vs Cost&lt;/strong>: Balance response quality with token costs&lt;/li>
&lt;li>&lt;strong>Gradual Rollouts&lt;/strong>: Use weighted routing for safe model updates&lt;/li>
&lt;li>&lt;strong>Environment Separation&lt;/strong>: Use headers for dev/staging/prod isolation&lt;/li>
&lt;li>&lt;strong>Content Awareness&lt;/strong>: Route based on request complexity&lt;/li>
&lt;li>&lt;strong>Fallback Planning&lt;/strong>: Always have backup routes for reliability&lt;/li>
&lt;/ol>
&lt;h3 id="monitoring-routing-health">Monitoring Routing Health&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; routing-health-check.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Routing Health Check&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;===================&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Check route statuses
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Route Status:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl get httproute -n enterprise-agentgateway -o custom-columns=\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;#34;NAME:.metadata.name,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">ACCEPTED:.status.parents[0].conditions[?(@.type==&amp;#39;Accepted&amp;#39;)].status,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">AGE:.metadata.creationTimestamp&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Check backend health
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Backend Status:&amp;#34; 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl get agentgatewaybackend -n enterprise-agentgateway -o custom-columns=\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;#34;NAME:.metadata.name,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">ACCEPTED:.status.conditions[?(@.type==&amp;#39;Accepted&amp;#39;)].status,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">AGE:.metadata.creationTimestamp&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Test each route with a quick health check
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Route Connectivity Tests:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">routes=(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/fast/completions&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/premium/completions&amp;#34; 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/flexible/completions?speed=fast&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/gradual-rollout/completions&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for route in &amp;#34;${routes[@]}&amp;#34;; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;%-35s: &amp;#34; &amp;#34;$route&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response_code=$(curl -s -o /dev/null -w &amp;#34;%{http_code}&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;${GATEWAY_IP:-localhost}:${GATEWAY_PORT:-8080}$route&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;test&amp;#34;}],&amp;#34;max_tokens&amp;#34;:1}&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [ &amp;#34;$response_code&amp;#34; = &amp;#34;200&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✓ OK&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✗ $response_code&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x routing-health-check.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./routing-health-check.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you want to clean up the routing configurations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove all routing test configurations&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute -n enterprise-agentgateway -l routing-type
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-gpt4o-mini openai-gpt4o openai-embeddings-route &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-dev-environment openai-prod-environment openai-ab-test &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-query-param-routing openai-weighted-routing &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-short-content openai-long-content openai-failover
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove test backends (keep original openai-all-models)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-gpt4o-mini openai-gpt4o openai-primary openai-fallback
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove policy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete enterpriseagentgatewaypolicy content-based-routing -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Clean up test scripts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -f test-weighted-routing.sh routing-analysis.sh comprehensive-routing-test.sh routing-health-check.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>With advanced routing patterns mastered, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Add security layers&lt;/strong> - Authentication, authorization, and rate limiting&lt;/li>
&lt;li>&lt;strong>Implement guardrails&lt;/strong> - Content filtering and safety policies&lt;/li>
&lt;li>&lt;strong>Multi-provider routing&lt;/strong> - Add Anthropic, AWS Bedrock, and others&lt;/li>
&lt;li>&lt;strong>Production optimization&lt;/strong> - Performance tuning and cost management&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll explore security features including JWT authentication, API key management, and role-based access control.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Intelligent routing&lt;/strong> transforms agentgateway into a sophisticated AI traffic manager&lt;/li>
&lt;li>&lt;strong>Multiple routing criteria&lt;/strong> enable complex decision-making logic&lt;/li>
&lt;li>&lt;strong>A/B testing and gradual rollouts&lt;/strong> provide safe model deployment strategies&lt;/li>
&lt;li>&lt;strong>Content-based routing&lt;/strong> optimizes cost and performance automatically&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> provides insights into routing effectiveness and patterns&lt;/li>
&lt;li>&lt;strong>Fallback strategies&lt;/strong> ensure reliability even when primary models fail&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway now has enterprise-grade routing capabilities that can handle complex production scenarios while optimizing for cost, performance, and reliability!&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>One of the most powerful features of agentgateway is its ability to intelligently route requests to different AI models or providers based on various criteria. This enables sophisticated scenarios like model selection based on request characteristics, A/B testing different models, and cost optimization through intelligent routing.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll explore multiple routing patterns that transform your agentgateway from a simple proxy into an intelligent AI traffic management system.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Path-based routing for different models and use cases&lt;/li>
&lt;li>Header-based routing for tenant isolation and testing&lt;/li>
&lt;li>Query parameter routing for flexible client control&lt;/li>
&lt;li>Weighted routing for A/B testing and gradual rollouts&lt;/li>
&lt;li>Content-based routing using request body analysis&lt;/li>
&lt;li>Fallback and failover routing strategies&lt;/li>
&lt;li>Cost-optimized routing patterns&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Completed previous blog posts (agentgateway setup, observability, OpenAI integration)&lt;/li>
&lt;li>Understanding of Kubernetes Gateway API concepts&lt;/li>
&lt;li>Knowledge of HTTP routing principles&lt;/li>
&lt;li>OpenAI API key for testing (we&amp;rsquo;ll add Anthropic optionally)&lt;/li>
&lt;/ul>
&lt;h2 id="environment-setup">Environment Setup&lt;/h2>
&lt;p>Ensure your environment is ready:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify environment variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-your-openai-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-license-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify agentgateway is running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify existing OpenAI configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-1-path-based-routing">Pattern 1: Path-Based Routing&lt;/h2>
&lt;h3 id="model-specific-paths">Model-Specific Paths&lt;/h3>
&lt;p>Create different paths for different models, allowing clients to choose the right model for their use case:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># GPT-4o Mini route (fast, cost-effective)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> use-case: general
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/fast
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;60s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Add model override header
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Use-Case
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: fast-general
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># GPT-4o route (premium quality)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: gpt-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> use-case: premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Use-Case
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: premium-quality
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Embeddings route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-embeddings-route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Service-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-model-specific-backends">Create Model-Specific Backends&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o-mini&amp;#34; # Force specific model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;60s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o&amp;#34; # Force premium model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-path-based-routing">Test Path-Based Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">localhost&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">8080&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test fast/general route (gpt-4o-mini)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Fast Route (GPT-4o Mini) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/fast/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What model are you and what are your strengths?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> usage: .usage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Premium Route (GPT-4o) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/premium/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What model are you and what are your strengths?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> usage: .usage
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-2-header-based-routing">Pattern 2: Header-Based Routing&lt;/h2>
&lt;h3 id="tenant-isolation-and-testing">Tenant Isolation and Testing&lt;/h3>
&lt;p>Use headers to route requests to different models or providers based on client characteristics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Development environment route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-dev-environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> environment: development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini # Use cheaper model for dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Tier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: economy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Production environment route 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-prod-environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> environment: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: prod
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o # Use premium model for prod
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Environment-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Tier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># A/B Testing route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-ab-test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> purpose: ab-testing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Variant A - GPT-4o Mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-a
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Variant
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-a
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Variant B - GPT-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Test
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-AB-Variant
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: variant-b
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Model-Used
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: gpt-4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-header-based-routing">Test Header-Based Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test development environment routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Development Environment ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-Environment: development&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from development!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Production Environment ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-Environment: production&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello from production!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> content: .choices[0].message.content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing A/B Test Variant A ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-AB-Test: variant-a&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;A/B test message&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 30
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;(X-AB-Variant|X-Model-Used)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing A/B Test Variant B ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;X-AB-Test: variant-b&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;A/B test message&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 30
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;(X-AB-Variant|X-Model-Used)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-3-query-parameter-routing">Pattern 3: Query Parameter Routing&lt;/h2>
&lt;h3 id="flexible-client-side-model-selection">Flexible Client-Side Model Selection&lt;/h3>
&lt;p>Allow clients to specify routing preferences via query parameters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-query-param-routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: query-parameter
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Route for speed preference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/flexible
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> queryParams:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: speed
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: fast
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Speed-Optimized
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Route for quality preference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/flexible
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> queryParams:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: quality
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: premium
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Quality-Optimized
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Route for cost preference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/flexible
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> queryParams:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: minimal
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Cost-Optimized
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-query-parameter-routing">Test Query Parameter Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test speed-optimized routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Speed-Optimized Routing ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/flexible/completions?speed=fast&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Fast response needed!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test quality-optimized routing &lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Quality-Optimized Routing ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/flexible/completions?quality=premium&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;High quality response needed!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test cost-optimized routing&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Cost-Optimized Routing ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/flexible/completions?cost=minimal&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Cost-effective response needed!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-4-weighted-routing-for-gradual-rollouts">Pattern 4: Weighted Routing for Gradual Rollouts&lt;/h2>
&lt;h3 id="implement-traffic-splitting">Implement Traffic Splitting&lt;/h3>
&lt;p>Use multiple backends with different weights for gradual model rollouts:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-weighted-routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: weighted
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/gradual-rollout
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Split traffic between models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # 80% traffic to stable model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # 20% traffic to new model for testing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Routing-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: weighted-rollout
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-weighted-routing">Test Weighted Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; test-weighted-routing.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_PORT=&amp;#34;${GATEWAY_PORT:-8080}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">REQUESTS=${1:-20}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Testing weighted routing with $REQUESTS requests...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Expected: ~80% gpt-4o-mini, ~20% gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">declare -A model_counts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for ((i=1; i&amp;lt;=REQUESTS; i++)); do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response=$(curl -s &amp;#34;$GATEWAY_IP:$GATEWAY_PORT/ai/gradual-rollout/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;What model are you?&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;max_tokens&amp;#34;: 10
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model=$(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.model&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ((model_counts[$model]++))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;Request %2d: %s\n&amp;#34; $i &amp;#34;$model&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;=== Results ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for model in &amp;#34;${!model_counts[@]}&amp;#34;; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> count=${model_counts[$model]}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> percentage=$(( count * 100 / REQUESTS ))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;$model: $count requests (${percentage}%)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x test-weighted-routing.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./test-weighted-routing.sh &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="pattern-5-content-based-routing">Pattern 5: Content-Based Routing&lt;/h2>
&lt;h3 id="route-based-on-request-content">Route Based on Request Content&lt;/h3>
&lt;p>Use agentgateway&amp;rsquo;s request analysis capabilities to route based on content characteristics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: enterpriseagentgateway.solo.io/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: EnterpriseAgentgatewayPolicy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: content-based-routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - group: gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> traffic:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Simple content analysis - route long requests to premium model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> requestTransformation:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> inline:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Add request length as header for routing decisions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> X-Content-Length: &amp;#39;string(len(json(request.body).messages[0].content))&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> X-Content-Type: &amp;#39;if len(json(request.body).messages[0].content) &amp;gt; 100 then &amp;#34;long&amp;#34; else &amp;#34;short&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Route short content to fast model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-short-content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: content-based
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> content-type: short
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/smart
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: short
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o-mini
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: short-content-fast-model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Route long content to premium model 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-long-content
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: content-based
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> content-type: long
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/smart
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> headers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: long
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-gpt4o
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Content-Routing
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: long-content-premium-model
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-content-based-routing">Test Content-Based Routing&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with short content&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Short Content (should use gpt-4o-mini) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/smart/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hi!&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;=== Testing Long Content (should use gpt-4o) ===&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/ai/smart/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;This is a much longer prompt that contains significantly more content and should trigger the routing logic to use the premium model because it requires more sophisticated processing and understanding. The content-based routing should detect this as a long request and route it to GPT-4o for better handling of complex queries that require more nuanced responses and deeper analysis.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{model: .model, usage: .usage}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="observing-routing-patterns">Observing Routing Patterns&lt;/h2>
&lt;h3 id="create-routing-dashboard">Create Routing Dashboard&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; routing-analysis.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Analyzing routing patterns from agentgateway logs...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Extract routing information from recent logs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail=100 | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq -r &amp;#39;select(.gen_ai) | [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .timestamp,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .route_name // &amp;#34;unknown&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.request.model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> (.gen_ai.usage.total_tokens // 0),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> (.request.headers[&amp;#34;x-environment&amp;#34;] // &amp;#34;none&amp;#34;),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> (.request.headers[&amp;#34;x-ab-test&amp;#34;] // &amp;#34;none&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ] | @csv&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> awk -F&amp;#39;,&amp;#39; &amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">BEGIN {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Route Analysis Report&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;====================&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_requests = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> split(&amp;#34;&amp;#34;, route_counts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> split(&amp;#34;&amp;#34;, model_counts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> split(&amp;#34;&amp;#34;, env_counts)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_requests++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Remove quotes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> route = $2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model = $3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tokens = $4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> env = $5
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, route)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, model) 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, env)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> route_counts[route]++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model_counts[model]++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (env != &amp;#34;none&amp;#34;) env_counts[env]++
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_tokens += tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">END {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Total Requests: &amp;#34; total_requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Total Tokens: &amp;#34; total_tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Routes:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for (route in route_counts) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34; %s: %d requests\n&amp;#34;, route, route_counts[route]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Models:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for (model in model_counts) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34; %s: %d requests\n&amp;#34;, model, model_counts[model]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Environments:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> for (env in env_counts) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34; %s: %d requests\n&amp;#34;, env, env_counts[env]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x routing-analysis.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./routing-analysis.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="monitor-routing-in-grafana">Monitor Routing in Grafana&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Open Grafana Dashboard&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n monitoring svc/grafana-prometheus 3000:3000 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Create Custom Routing Queries&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Request rate by route&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">agentgateway_requests_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">route_name&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Request rate by model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">agentgateway_requests_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">model&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1"># Token usage by routing pattern&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="kr">rate&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">agentgateway_tokens_total&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s">5m&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="o">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">route_name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">token_type&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Watch routing patterns&lt;/strong> as you send test requests through different patterns&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="pattern-6-fallback-and-failover-routing">Pattern 6: Fallback and Failover Routing&lt;/h2>
&lt;h3 id="primary-secondary-backend-configuration">Primary-Secondary Backend Configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Primary backend with health checking
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-primary
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Health checking configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> healthCheck:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> interval: &amp;#34;30s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout: &amp;#34;5s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> unhealthyThreshold: 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> healthyThreshold: 2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Fallback backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-fallback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Failover route configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-failover
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> routing-type: failover
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /ai/reliable
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Primary backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-primary
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Fallback backend (only used if primary fails)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-fallback
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> weight: 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: ResponseHeaderModifier
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> responseHeaderModifier:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: X-Routing-Type
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: failover-enabled
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="creating-routing-test-suite">Creating Routing Test Suite&lt;/h2>
&lt;h3 id="comprehensive-routing-test">Comprehensive Routing Test&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; comprehensive-routing-test.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_PORT=&amp;#34;${GATEWAY_PORT:-8080}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Comprehensive Routing Pattern Test Suite&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;=======================================&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local name=&amp;#34;$1&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local endpoint=&amp;#34;$2&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local headers=&amp;#34;$3&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local expected_model=&amp;#34;$4&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Testing: $name&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Endpoint: $endpoint&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local response=$(curl -s $headers &amp;#34;$GATEWAY_IP:$GATEWAY_PORT$endpoint&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;What model are you?&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;max_tokens&amp;#34;: 20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local actual_model=$(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.model&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local tokens=$(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.usage.total_tokens&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [[ &amp;#34;$actual_model&amp;#34; == *&amp;#34;$expected_model&amp;#34;* ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✓ Success: $actual_model ($tokens tokens)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✗ Failed: Expected $expected_model, got $actual_model&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;1. Path-based routing tests&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;---------------------------&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Fast Route&amp;#34; &amp;#34;/ai/fast/completions&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Premium Route&amp;#34; &amp;#34;/ai/premium/completions&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;2. Header-based routing tests&amp;#34; 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;-----------------------------&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Dev Environment&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-Environment: development&amp;#39;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Prod Environment&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-Environment: production&amp;#39;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;A/B Test Variant A&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-AB-Test: variant-a&amp;#39;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;A/B Test Variant B&amp;#34; &amp;#34;/ai/chat/completions&amp;#34; &amp;#34;-H &amp;#39;X-AB-Test: variant-b&amp;#39;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;3. Query parameter routing tests&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;--------------------------------&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Speed Optimized&amp;#34; &amp;#34;/ai/flexible/completions?speed=fast&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Quality Optimized&amp;#34; &amp;#34;/ai/flexible/completions?quality=premium&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;Cost Optimized&amp;#34; &amp;#34;/ai/flexible/completions?cost=minimal&amp;#34; &amp;#34;&amp;#34; &amp;#34;gpt-4o-mini&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Routing test suite complete!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Check Grafana dashboard to see routing patterns and metrics.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x comprehensive-routing-test.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./comprehensive-routing-test.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="best-practices-and-considerations">Best Practices and Considerations&lt;/h2>
&lt;h3 id="routing-strategy-guidelines">Routing Strategy Guidelines&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Performance vs Cost&lt;/strong>: Balance response quality with token costs&lt;/li>
&lt;li>&lt;strong>Gradual Rollouts&lt;/strong>: Use weighted routing for safe model updates&lt;/li>
&lt;li>&lt;strong>Environment Separation&lt;/strong>: Use headers for dev/staging/prod isolation&lt;/li>
&lt;li>&lt;strong>Content Awareness&lt;/strong>: Route based on request complexity&lt;/li>
&lt;li>&lt;strong>Fallback Planning&lt;/strong>: Always have backup routes for reliability&lt;/li>
&lt;/ol>
&lt;h3 id="monitoring-routing-health">Monitoring Routing Health&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; routing-health-check.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Routing Health Check&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;===================&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Check route statuses
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Route Status:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl get httproute -n enterprise-agentgateway -o custom-columns=\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;#34;NAME:.metadata.name,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">ACCEPTED:.status.parents[0].conditions[?(@.type==&amp;#39;Accepted&amp;#39;)].status,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">AGE:.metadata.creationTimestamp&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Check backend health
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Backend Status:&amp;#34; 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl get agentgatewaybackend -n enterprise-agentgateway -o custom-columns=\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">&amp;#34;NAME:.metadata.name,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">ACCEPTED:.status.conditions[?(@.type==&amp;#39;Accepted&amp;#39;)].status,\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">AGE:.metadata.creationTimestamp&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Test each route with a quick health check
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Route Connectivity Tests:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">routes=(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/fast/completions&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/premium/completions&amp;#34; 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/flexible/completions?speed=fast&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;/ai/gradual-rollout/completions&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for route in &amp;#34;${routes[@]}&amp;#34;; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;%-35s: &amp;#34; &amp;#34;$route&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response_code=$(curl -s -o /dev/null -w &amp;#34;%{http_code}&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;${GATEWAY_IP:-localhost}:${GATEWAY_PORT:-8080}$route&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{&amp;#34;messages&amp;#34;:[{&amp;#34;role&amp;#34;:&amp;#34;user&amp;#34;,&amp;#34;content&amp;#34;:&amp;#34;test&amp;#34;}],&amp;#34;max_tokens&amp;#34;:1}&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [ &amp;#34;$response_code&amp;#34; = &amp;#34;200&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✓ OK&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;✗ $response_code&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x routing-health-check.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./routing-health-check.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you want to clean up the routing configurations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove all routing test configurations&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute -n enterprise-agentgateway -l routing-type
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete httproute -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-gpt4o-mini openai-gpt4o openai-embeddings-route &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-dev-environment openai-prod-environment openai-ab-test &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-query-param-routing openai-weighted-routing &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-short-content openai-long-content openai-failover
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove test backends (keep original openai-all-models)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete agentgatewaybackend -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> openai-gpt4o-mini openai-gpt4o openai-primary openai-fallback
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Remove policy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete enterpriseagentgatewaypolicy content-based-routing -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Clean up test scripts&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rm -f test-weighted-routing.sh routing-analysis.sh comprehensive-routing-test.sh routing-health-check.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>With advanced routing patterns mastered, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Add security layers&lt;/strong> - Authentication, authorization, and rate limiting&lt;/li>
&lt;li>&lt;strong>Implement guardrails&lt;/strong> - Content filtering and safety policies&lt;/li>
&lt;li>&lt;strong>Multi-provider routing&lt;/strong> - Add Anthropic, AWS Bedrock, and others&lt;/li>
&lt;li>&lt;strong>Production optimization&lt;/strong> - Performance tuning and cost management&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll explore security features including JWT authentication, API key management, and role-based access control.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Intelligent routing&lt;/strong> transforms agentgateway into a sophisticated AI traffic manager&lt;/li>
&lt;li>&lt;strong>Multiple routing criteria&lt;/strong> enable complex decision-making logic&lt;/li>
&lt;li>&lt;strong>A/B testing and gradual rollouts&lt;/strong> provide safe model deployment strategies&lt;/li>
&lt;li>&lt;strong>Content-based routing&lt;/strong> optimizes cost and performance automatically&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> provides insights into routing effectiveness and patterns&lt;/li>
&lt;li>&lt;strong>Fallback strategies&lt;/strong> ensure reliability even when primary models fail&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway now has enterprise-grade routing capabilities that can handle complex production scenarios while optimizing for cost, performance, and reliability!&lt;/p></content:encoded></item><item><title>Observability Stack: Monitoring Your AI Gateway</title><link>https://maniak.io/articles/02-observability-stack-monitoring-your-ai-gateway/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/02-observability-stack-monitoring-your-ai-gateway/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Observability is crucial for any production AI Gateway deployment. Enterprise agentgateway emits comprehensive OpenTelemetry-compatible metrics, logs, and traces out of the box. In this guide, we&amp;rsquo;ll deploy a complete observability stack including Grafana, Prometheus, Tempo, and Loki to collect, store, and visualize this rich telemetry data.&lt;/p>
&lt;p>This setup will give you real-time visibility into your AI Gateway&amp;rsquo;s performance, cost metrics, token usage, streaming performance, and more.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Deploy Tempo for distributed tracing&lt;/li>
&lt;li>Install Prometheus and Grafana for metrics and visualization&lt;/li>
&lt;li>Configure agentgateway-specific monitoring&lt;/li>
&lt;li>Set up the official agentgateway Grafana dashboard&lt;/li>
&lt;li>Access and interpret observability data&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Kind cluster with Enterprise agentgateway installed (from Part 1)&lt;/li>
&lt;li>kubectl configured to work with your cluster&lt;/li>
&lt;li>Helm 3.x installed&lt;/li>
&lt;/ul>
&lt;h2 id="architecture-overview">Architecture Overview&lt;/h2>
&lt;p>Our observability stack will include:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Grafana&lt;/strong>: Visualization and dashboards&lt;/li>
&lt;li>&lt;strong>Prometheus&lt;/strong>: Metrics collection and storage&lt;/li>
&lt;li>&lt;strong>Tempo&lt;/strong>: Distributed tracing backend&lt;/li>
&lt;li>&lt;strong>Loki&lt;/strong>: Log aggregation (optional)&lt;/li>
&lt;li>&lt;strong>agentgateway Datasources&lt;/strong>: Pre-configured Grafana dashboards&lt;/li>
&lt;/ul>
&lt;h2 id="deploy-monitoring-stack">Deploy Monitoring Stack&lt;/h2>
&lt;h3 id="create-monitoring-namespace">Create Monitoring Namespace&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create namespace monitoring
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-prometheus">Install Prometheus&lt;/h3>
&lt;p>First, let&amp;rsquo;s deploy Prometheus to collect metrics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install prometheus prometheus-community/prometheus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set server.service.type&lt;span class="o">=&lt;/span>NodePort &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set server.service.nodePort&lt;span class="o">=&lt;/span>&lt;span class="m">30090&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set server.persistentVolume.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set alertmanager.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set kube-state-metrics.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set nodeExporter.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set pushgateway.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-tempo">Install Tempo&lt;/h3>
&lt;p>Deploy Tempo for distributed tracing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm repo add grafana https://grafana.github.io/helm-charts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create Tempo configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF &amp;gt; tempo-values.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">tempo:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retention: 24h
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> receivers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jaeger:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:14250
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> thrift_http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:14268
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> otlp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:4318
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">storage:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> trace:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backend: local
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /var/tempo/traces
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">compactor:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retention: 24h
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metrics_generator:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> registry:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> external_labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> source: tempo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install tempo grafana/tempo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --values tempo-values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-grafana">Install Grafana&lt;/h3>
&lt;p>Deploy Grafana with pre-configured datasources:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create Grafana configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF &amp;gt; grafana-values.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">persistence:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">datasources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> datasources.yaml:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiVersion: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> datasources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: Prometheus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: prometheus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> url: http://prometheus-server:80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> access: proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> isDefault: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: Tempo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: tempo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> url: http://tempo:3100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> access: proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">dashboardProviders:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> dashboardproviders.yaml:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiVersion: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> providers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: &amp;#39;default&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> orgId: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> folder: &amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: file
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> disableDeletion: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> editable: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> options:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /var/lib/grafana/dashboards/default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">dashboards:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> default:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> agentgateway-genai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gnetId: 21703
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> revision: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> datasource: Prometheus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">adminPassword: admin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install grafana grafana/grafana &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --values grafana-values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="configure-agentgateway-for-observability">Configure agentgateway for Observability&lt;/h2>
&lt;h3 id="update-agentgateway-configuration">Update agentgateway Configuration&lt;/h3>
&lt;p>We need to configure agentgateway to send traces to our Tempo instance:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: enterpriseagentgateway.solo.io/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-params
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Enable shared extensions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sharedExtensions:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extauth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ratelimiter:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extCache:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Enhanced observability configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rawConfig:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> logging:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> level: info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body: json(request.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: json(response.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body.modelId: json(request.body).modelId
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.provider: &amp;#39;llm.provider&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.model: &amp;#39;llm.requestModel&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.tokens.input: &amp;#39;llm.inputTokens&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.tokens.output: &amp;#39;llm.outputTokens&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.total: &amp;#39;llm.totalCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> format: json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tracing:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> randomSampling: &amp;#39;true&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> collector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: tempo.monitoring.svc.cluster.local:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # GenAI semantic conventions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.operation.name: &amp;#39;&amp;#34;chat&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.system: &amp;#34;llm.provider&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.prompt: &amp;#39;llm.prompt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.completion: &amp;#39;llm.completion.map(c, {&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;, &amp;#34;content&amp;#34;: c})&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request.model: &amp;#34;llm.requestModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.response.model: &amp;#34;llm.responseModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.completion_tokens: &amp;#34;llm.outputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.prompt_tokens: &amp;#34;llm.inputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request: &amp;#39;flatten(llm.params)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Additional context
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: &amp;#39;json(response.body)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.total: &amp;#39;llm.totalCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.input: &amp;#39;llm.inputCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.output: &amp;#39;llm.outputCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metrics:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> prefix: &amp;#34;agentgateway&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tags:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: &amp;#39;llm.provider&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#39;llm.requestModel&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> user: &amp;#39;jwt.sub // &amp;#34;anonymous&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Service configuration for monitoring
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: metrics
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 9091
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 9091
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30091
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Deployment configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> resources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> requests:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 200m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 128Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> limits:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 500m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 256Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="verify-monitoring-stack">Verify Monitoring Stack&lt;/h2>
&lt;h3 id="check-all-pods-are-running">Check All Pods are Running&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n monitoring
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grafana-7b8c4c4d4c-xyz12 1/1 Running &lt;span class="m">0&lt;/span> 3m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">prometheus-server-5f8b8b7d7d-abc34 1/1 Running &lt;span class="m">0&lt;/span> 5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tempo-0 1/1 Running &lt;span class="m">0&lt;/span> 4m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-agentgateway-configuration">Verify agentgateway Configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail&lt;span class="o">=&lt;/span>&lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Look for log entries indicating successful startup and trace collection.&lt;/p>
&lt;h2 id="access-monitoring-interfaces">Access Monitoring Interfaces&lt;/h2>
&lt;h3 id="grafana-dashboard">Grafana Dashboard&lt;/h3>
&lt;p>Access Grafana at: &lt;code>http://localhost:30080&lt;/code>&lt;/p>
&lt;ul>
&lt;li>Username: &lt;code>admin&lt;/code>&lt;/li>
&lt;li>Password: &lt;code>admin&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="prometheus-ui">Prometheus UI&lt;/h3>
&lt;p>Access Prometheus at: &lt;code>http://localhost:30090&lt;/code>&lt;/p>
&lt;h3 id="agentgateway-metrics">agentgateway Metrics&lt;/h3>
&lt;p>Access agentgateway metrics directly: &lt;code>http://localhost:30091/metrics&lt;/code>&lt;/p>
&lt;h2 id="agentgateway-genai-dashboard">agentgateway GenAI Dashboard&lt;/h2>
&lt;p>The Grafana deployment automatically imports the official agentgateway dashboard (ID: 21703). This dashboard provides:&lt;/p>
&lt;h3 id="key-metrics-panels">Key Metrics Panels&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Request Rate and Latency&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Requests per second by provider&lt;/li>
&lt;li>P95, P99 latency percentiles&lt;/li>
&lt;li>Error rates and status codes&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Token Usage and Costs&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Input/output token consumption&lt;/li>
&lt;li>Cost tracking per provider&lt;/li>
&lt;li>Token efficiency metrics&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Model Performance&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Response time by model&lt;/li>
&lt;li>Token generation rates&lt;/li>
&lt;li>Streaming performance&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Provider Health&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Provider availability&lt;/li>
&lt;li>Error rates by provider&lt;/li>
&lt;li>Failover events&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="using-the-dashboard">Using the Dashboard&lt;/h3>
&lt;ol>
&lt;li>Navigate to &lt;strong>Dashboards &amp;gt; Browse&lt;/strong> in Grafana&lt;/li>
&lt;li>Open &lt;strong>agentgateway GenAI Dashboard&lt;/strong>&lt;/li>
&lt;li>Set time range (e.g., Last 1 hour)&lt;/li>
&lt;li>Select providers/models using dropdown filters&lt;/li>
&lt;/ol>
&lt;h2 id="testing-your-monitoring-setup">Testing Your Monitoring Setup&lt;/h2>
&lt;p>Let&amp;rsquo;s create some test traffic to see data flowing through our observability stack:&lt;/p>
&lt;h3 id="deploy-test-mock-backend">Deploy Test Mock Backend&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: wiremock
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: wiremock/wiremock:3.3.1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - --port=8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - --verbose
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumeMounts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: wiremock-data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mountPath: /home/wiremock
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> env:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: JAVA_OPTS
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;-Dfile.encoding=UTF-8&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: wiremock-data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> configMap:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: ConfigMap
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">data:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mappings.json: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;mappings&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;request&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;method&amp;#34;: &amp;#34;POST&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;url&amp;#34;: &amp;#34;/v1/chat/completions&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;response&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;status&amp;#34;: 200,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;headers&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;Content-Type&amp;#34;: &amp;#34;application/json&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;jsonBody&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;id&amp;#34;: &amp;#34;chatcmpl-test-{{randomValue type=&amp;#39;ALPHANUMERIC&amp;#39; length=10}}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;object&amp;#34;: &amp;#34;chat.completion&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;created&amp;#34;: {{currentTimestamp}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;choices&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;index&amp;#34;: 0,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;message&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;role&amp;#34;: &amp;#34;assistant&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;content&amp;#34;: &amp;#34;Hello! This is a mock response from the test OpenAI backend. Current time: {{currentTimestamp}}.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;finish_reason&amp;#34;: &amp;#34;stop&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;usage&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;prompt_tokens&amp;#34;: 25,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;completion_tokens&amp;#34;: 15,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;total_tokens&amp;#34;: 40
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;transformers&amp;#34;: [&amp;#34;response-template&amp;#34;],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;fixedDelayMilliseconds&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="configure-agentgateway-route">Configure agentgateway Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> connectionString: http://mock-openai.default.svc.cluster.local
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> authToken:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> key: api-key
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> default: gpt-4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mapping:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;gpt-4&amp;#34;: gpt-4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;gpt-3.5-turbo&amp;#34;: gpt-3.5-turbo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">type: Opaque
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">stringData:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> api-key: &amp;#34;test-api-key&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostnames:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;*&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplaceFullPath
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replaceFullPath: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mock-openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="generate-test-traffic">Generate Test Traffic&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Send test requests to generate observability data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> i in &lt;span class="o">{&lt;/span>1..10&lt;span class="o">}&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> curl -X POST http://localhost:8080/openai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer test-token&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Test message &amp;#39;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">i&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">&amp;#39; for observability demo&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sleep &lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="viewing-observability-data">Viewing Observability Data&lt;/h2>
&lt;h3 id="in-grafana">In Grafana&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Dashboard Overview&lt;/strong>: Navigate to the agentgateway dashboard&lt;/li>
&lt;li>&lt;strong>Request Metrics&lt;/strong>: See request rates, response times&lt;/li>
&lt;li>&lt;strong>Token Usage&lt;/strong>: Monitor input/output tokens and costs&lt;/li>
&lt;li>&lt;strong>Error Analysis&lt;/strong>: Check error rates and types&lt;/li>
&lt;/ol>
&lt;h3 id="in-tempo-distributed-tracing">In Tempo (Distributed Tracing)&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Go to Explore&lt;/strong> in Grafana&lt;/li>
&lt;li>&lt;strong>Select Tempo&lt;/strong> datasource&lt;/li>
&lt;li>&lt;strong>Search for traces&lt;/strong> by service name: &lt;code>agentgateway&lt;/code>&lt;/li>
&lt;li>&lt;strong>Analyze trace spans&lt;/strong> showing request flow&lt;/li>
&lt;/ol>
&lt;h3 id="key-traces-to-look-for">Key Traces to Look For&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>HTTP Request Span&lt;/strong>: Gateway ingress&lt;/li>
&lt;li>&lt;strong>LLM Provider Span&lt;/strong>: Backend communication&lt;/li>
&lt;li>&lt;strong>Auth Span&lt;/strong>: Authentication processing&lt;/li>
&lt;li>&lt;strong>Rate Limit Span&lt;/strong>: Rate limiting decisions&lt;/li>
&lt;/ul>
&lt;h3 id="sample-tempo-query">Sample Tempo Query&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{service.name=&amp;#34;agentgateway&amp;#34;}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="advanced-monitoring-configuration">Advanced Monitoring Configuration&lt;/h2>
&lt;h3 id="custom-metrics">Custom Metrics&lt;/h3>
&lt;p>Add custom metrics to track specific business KPIs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">customMetrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;user_requests_total&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;counter&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">help&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Total requests per user&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;jwt.sub // &amp;#34;anonymous&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.requestModel&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;token_cost_dollars&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;histogram&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">help&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Cost in dollars per request&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">buckets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="m">0.001&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.01&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.totalCost&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="alert-rules">Alert Rules&lt;/h3>
&lt;p>Create Prometheus alert rules for critical conditions:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">groups&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.rules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HighErrorRate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rate(agentgateway_requests_total{status=~&amp;#34;5..&amp;#34;}[5m]) &amp;gt; 0.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;High error rate detected&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HighLatency&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">histogram_quantile(0.95, rate(agentgateway_request_duration_seconds_bucket[5m])) &amp;gt; 5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;High latency detected&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HighCost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">increase(agentgateway_token_cost_dollars_total[1h]) &amp;gt; 50&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">0m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;High hourly cost detected&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="monitoring-best-practices">Monitoring Best Practices&lt;/h2>
&lt;h3 id="resource-planning">Resource Planning&lt;/h3>
&lt;p>Monitor these key metrics for capacity planning:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Request Rate&lt;/strong>: Track requests/second trends&lt;/li>
&lt;li>&lt;strong>Token Velocity&lt;/strong>: Monitor tokens/minute per model&lt;/li>
&lt;li>&lt;strong>Cost Burn Rate&lt;/strong>: Track $/hour consumption&lt;/li>
&lt;li>&lt;strong>Provider Latency&lt;/strong>: Monitor P95/P99 response times&lt;/li>
&lt;/ol>
&lt;h3 id="performance-optimization">Performance Optimization&lt;/h3>
&lt;p>Use observability data to optimize:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Route Configuration&lt;/strong>: Based on latency patterns&lt;/li>
&lt;li>&lt;strong>Caching Strategies&lt;/strong>: Based on request patterns&lt;/li>
&lt;li>&lt;strong>Rate Limiting&lt;/strong>: Based on usage distribution&lt;/li>
&lt;li>&lt;strong>Provider Selection&lt;/strong>: Based on cost/performance&lt;/li>
&lt;/ol>
&lt;h3 id="cost-management">Cost Management&lt;/h3>
&lt;p>Track and alert on:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Daily/Monthly spend&lt;/strong> per provider&lt;/li>
&lt;li>&lt;strong>Cost per user/team&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Token efficiency&lt;/strong> (output/input ratio)&lt;/li>
&lt;li>&lt;strong>Most expensive models/users&lt;/strong>&lt;/li>
&lt;/ol>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;h3 id="missing-traces">Missing Traces&lt;/h3>
&lt;p>If traces aren&amp;rsquo;t appearing in Tempo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Tempo is receiving traces&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>tempo -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify agentgateway trace configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get enterpriseagentgatewayparameters agentgateway-params -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check connectivity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n enterprise-agentgateway deployment/agentgateway -- nc -zv tempo.monitoring.svc.cluster.local &lt;span class="m">4317&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="missing-metrics">Missing Metrics&lt;/h3>
&lt;p>If metrics aren&amp;rsquo;t showing in Prometheus:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Prometheus targets&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://localhost:30090/targets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify agentgateway metrics endpoint&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://localhost:30091/metrics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Prometheus configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get configmap prometheus-server -n monitoring -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="dashboard-issues">Dashboard Issues&lt;/h3>
&lt;p>If the dashboard isn&amp;rsquo;t loading:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Grafana pod logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>grafana -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify datasource connectivity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n monitoring deployment/grafana -- nc -zv prometheus-server &lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n monitoring deployment/grafana -- nc -zv tempo &lt;span class="m">3100&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>To remove the monitoring stack:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm uninstall grafana -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall prometheus -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall tempo -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete namespace monitoring
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>With comprehensive observability in place, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Set up development environments&lt;/strong> with mock providers&lt;/li>
&lt;li>&lt;strong>Configure real AI provider integrations&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Implement advanced routing strategies&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Add security and rate limiting policies&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll create a mock OpenAI environment for cost-free development and testing.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Complete visibility&lt;/strong> into AI Gateway performance and costs&lt;/li>
&lt;li>&lt;strong>Real-time monitoring&lt;/strong> with Grafana dashboards&lt;/li>
&lt;li>&lt;strong>Distributed tracing&lt;/strong> for request flow analysis&lt;/li>
&lt;li>&lt;strong>Cost tracking&lt;/strong> and optimization insights&lt;/li>
&lt;li>&lt;strong>Production-ready&lt;/strong> monitoring stack for kind clusters&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway is now fully instrumented and ready for production workloads with comprehensive observability!&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Observability is crucial for any production AI Gateway deployment. Enterprise agentgateway emits comprehensive OpenTelemetry-compatible metrics, logs, and traces out of the box. In this guide, we&amp;rsquo;ll deploy a complete observability stack including Grafana, Prometheus, Tempo, and Loki to collect, store, and visualize this rich telemetry data.&lt;/p>
&lt;p>This setup will give you real-time visibility into your AI Gateway&amp;rsquo;s performance, cost metrics, token usage, streaming performance, and more.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Deploy Tempo for distributed tracing&lt;/li>
&lt;li>Install Prometheus and Grafana for metrics and visualization&lt;/li>
&lt;li>Configure agentgateway-specific monitoring&lt;/li>
&lt;li>Set up the official agentgateway Grafana dashboard&lt;/li>
&lt;li>Access and interpret observability data&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Kind cluster with Enterprise agentgateway installed (from Part 1)&lt;/li>
&lt;li>kubectl configured to work with your cluster&lt;/li>
&lt;li>Helm 3.x installed&lt;/li>
&lt;/ul>
&lt;h2 id="architecture-overview">Architecture Overview&lt;/h2>
&lt;p>Our observability stack will include:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Grafana&lt;/strong>: Visualization and dashboards&lt;/li>
&lt;li>&lt;strong>Prometheus&lt;/strong>: Metrics collection and storage&lt;/li>
&lt;li>&lt;strong>Tempo&lt;/strong>: Distributed tracing backend&lt;/li>
&lt;li>&lt;strong>Loki&lt;/strong>: Log aggregation (optional)&lt;/li>
&lt;li>&lt;strong>agentgateway Datasources&lt;/strong>: Pre-configured Grafana dashboards&lt;/li>
&lt;/ul>
&lt;h2 id="deploy-monitoring-stack">Deploy Monitoring Stack&lt;/h2>
&lt;h3 id="create-monitoring-namespace">Create Monitoring Namespace&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl create namespace monitoring
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-prometheus">Install Prometheus&lt;/h3>
&lt;p>First, let&amp;rsquo;s deploy Prometheus to collect metrics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install prometheus prometheus-community/prometheus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set server.service.type&lt;span class="o">=&lt;/span>NodePort &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set server.service.nodePort&lt;span class="o">=&lt;/span>&lt;span class="m">30090&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set server.persistentVolume.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set alertmanager.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set kube-state-metrics.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set nodeExporter.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set pushgateway.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-tempo">Install Tempo&lt;/h3>
&lt;p>Deploy Tempo for distributed tracing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm repo add grafana https://grafana.github.io/helm-charts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo update
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create Tempo configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF &amp;gt; tempo-values.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">tempo:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retention: 24h
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> receivers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jaeger:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:14250
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> thrift_http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:14268
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> otlp:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocols:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> grpc:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> http:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: 0.0.0.0:4318
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">storage:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> trace:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backend: local
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /var/tempo/traces
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">compactor:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retention: 24h
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metrics_generator:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> registry:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> external_labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> source: tempo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install tempo grafana/tempo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --values tempo-values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="install-grafana">Install Grafana&lt;/h3>
&lt;p>Deploy Grafana with pre-configured datasources:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create Grafana configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF &amp;gt; grafana-values.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">persistence:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">datasources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> datasources.yaml:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiVersion: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> datasources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: Prometheus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: prometheus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> url: http://prometheus-server:80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> access: proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> isDefault: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: Tempo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: tempo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> url: http://tempo:3100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> access: proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">dashboardProviders:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> dashboardproviders.yaml:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> apiVersion: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> providers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: &amp;#39;default&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> orgId: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> folder: &amp;#39;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: file
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> disableDeletion: false
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> editable: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> options:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path: /var/lib/grafana/dashboards/default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">dashboards:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> default:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> agentgateway-genai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gnetId: 21703
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> revision: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> datasource: Prometheus
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">adminPassword: admin
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install grafana grafana/grafana &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --values grafana-values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="configure-agentgateway-for-observability">Configure agentgateway for Observability&lt;/h2>
&lt;h3 id="update-agentgateway-configuration">Update agentgateway Configuration&lt;/h3>
&lt;p>We need to configure agentgateway to send traces to our Tempo instance:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: enterpriseagentgateway.solo.io/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-params
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Enable shared extensions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sharedExtensions:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extauth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ratelimiter:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extCache:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Enhanced observability configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rawConfig:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> logging:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> level: info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body: json(request.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: json(response.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body.modelId: json(request.body).modelId
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.provider: &amp;#39;llm.provider&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.model: &amp;#39;llm.requestModel&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.tokens.input: &amp;#39;llm.inputTokens&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.tokens.output: &amp;#39;llm.outputTokens&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.total: &amp;#39;llm.totalCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> format: json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tracing:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> randomSampling: &amp;#39;true&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> collector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: tempo.monitoring.svc.cluster.local:4317
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # GenAI semantic conventions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.operation.name: &amp;#39;&amp;#34;chat&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.system: &amp;#34;llm.provider&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.prompt: &amp;#39;llm.prompt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.completion: &amp;#39;llm.completion.map(c, {&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;, &amp;#34;content&amp;#34;: c})&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request.model: &amp;#34;llm.requestModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.response.model: &amp;#34;llm.responseModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.completion_tokens: &amp;#34;llm.outputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.prompt_tokens: &amp;#34;llm.inputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request: &amp;#39;flatten(llm.params)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Additional context
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: &amp;#39;json(response.body)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.total: &amp;#39;llm.totalCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.input: &amp;#39;llm.inputCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> llm.cost.output: &amp;#39;llm.outputCost&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metrics:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> prefix: &amp;#34;agentgateway&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tags:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: &amp;#39;llm.provider&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model: &amp;#39;llm.requestModel&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> user: &amp;#39;jwt.sub // &amp;#34;anonymous&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Service configuration for monitoring
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: metrics
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 9091
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 9091
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30091
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Deployment configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> resources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> requests:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 200m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 128Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> limits:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 500m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 256Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="verify-monitoring-stack">Verify Monitoring Stack&lt;/h2>
&lt;h3 id="check-all-pods-are-running">Check All Pods are Running&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n monitoring
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grafana-7b8c4c4d4c-xyz12 1/1 Running &lt;span class="m">0&lt;/span> 3m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">prometheus-server-5f8b8b7d7d-abc34 1/1 Running &lt;span class="m">0&lt;/span> 5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tempo-0 1/1 Running &lt;span class="m">0&lt;/span> 4m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-agentgateway-configuration">Verify agentgateway Configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail&lt;span class="o">=&lt;/span>&lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Look for log entries indicating successful startup and trace collection.&lt;/p>
&lt;h2 id="access-monitoring-interfaces">Access Monitoring Interfaces&lt;/h2>
&lt;h3 id="grafana-dashboard">Grafana Dashboard&lt;/h3>
&lt;p>Access Grafana at: &lt;code>http://localhost:30080&lt;/code>&lt;/p>
&lt;ul>
&lt;li>Username: &lt;code>admin&lt;/code>&lt;/li>
&lt;li>Password: &lt;code>admin&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="prometheus-ui">Prometheus UI&lt;/h3>
&lt;p>Access Prometheus at: &lt;code>http://localhost:30090&lt;/code>&lt;/p>
&lt;h3 id="agentgateway-metrics">agentgateway Metrics&lt;/h3>
&lt;p>Access agentgateway metrics directly: &lt;code>http://localhost:30091/metrics&lt;/code>&lt;/p>
&lt;h2 id="agentgateway-genai-dashboard">agentgateway GenAI Dashboard&lt;/h2>
&lt;p>The Grafana deployment automatically imports the official agentgateway dashboard (ID: 21703). This dashboard provides:&lt;/p>
&lt;h3 id="key-metrics-panels">Key Metrics Panels&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Request Rate and Latency&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Requests per second by provider&lt;/li>
&lt;li>P95, P99 latency percentiles&lt;/li>
&lt;li>Error rates and status codes&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Token Usage and Costs&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Input/output token consumption&lt;/li>
&lt;li>Cost tracking per provider&lt;/li>
&lt;li>Token efficiency metrics&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Model Performance&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Response time by model&lt;/li>
&lt;li>Token generation rates&lt;/li>
&lt;li>Streaming performance&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Provider Health&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Provider availability&lt;/li>
&lt;li>Error rates by provider&lt;/li>
&lt;li>Failover events&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="using-the-dashboard">Using the Dashboard&lt;/h3>
&lt;ol>
&lt;li>Navigate to &lt;strong>Dashboards &amp;gt; Browse&lt;/strong> in Grafana&lt;/li>
&lt;li>Open &lt;strong>agentgateway GenAI Dashboard&lt;/strong>&lt;/li>
&lt;li>Set time range (e.g., Last 1 hour)&lt;/li>
&lt;li>Select providers/models using dropdown filters&lt;/li>
&lt;/ol>
&lt;h2 id="testing-your-monitoring-setup">Testing Your Monitoring Setup&lt;/h2>
&lt;p>Let&amp;rsquo;s create some test traffic to see data flowing through our observability stack:&lt;/p>
&lt;h3 id="deploy-test-mock-backend">Deploy Test Mock Backend&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: apps/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Deployment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> matchLabels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: wiremock
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> image: wiremock/wiremock:3.3.1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> args:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - --port=8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - --verbose
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumeMounts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: wiremock-data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mountPath: /home/wiremock
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> env:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: JAVA_OPTS
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: &amp;#34;-Dfile.encoding=UTF-8&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> volumes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: wiremock-data
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> configMap:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Service
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> selector:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> app: mock-openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: ConfigMap
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-config
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">data:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mappings.json: |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;mappings&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;request&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;method&amp;#34;: &amp;#34;POST&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;url&amp;#34;: &amp;#34;/v1/chat/completions&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;response&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;status&amp;#34;: 200,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;headers&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;Content-Type&amp;#34;: &amp;#34;application/json&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;jsonBody&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;id&amp;#34;: &amp;#34;chatcmpl-test-{{randomValue type=&amp;#39;ALPHANUMERIC&amp;#39; length=10}}&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;object&amp;#34;: &amp;#34;chat.completion&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;created&amp;#34;: {{currentTimestamp}},
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;choices&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;index&amp;#34;: 0,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;message&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;role&amp;#34;: &amp;#34;assistant&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;content&amp;#34;: &amp;#34;Hello! This is a mock response from the test OpenAI backend. Current time: {{currentTimestamp}}.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;finish_reason&amp;#34;: &amp;#34;stop&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;usage&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;prompt_tokens&amp;#34;: 25,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;completion_tokens&amp;#34;: 15,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;total_tokens&amp;#34;: 40
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;transformers&amp;#34;: [&amp;#34;response-template&amp;#34;],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;fixedDelayMilliseconds&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="configure-agentgateway-route">Configure agentgateway Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> connectionString: http://mock-openai.default.svc.cluster.local
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> authToken:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> key: api-key
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> default: gpt-4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> mapping:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;gpt-4&amp;#34;: gpt-4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;gpt-3.5-turbo&amp;#34;: gpt-3.5-turbo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">type: Opaque
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">stringData:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> api-key: &amp;#34;test-api-key&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: mock-openai-route
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: default
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostnames:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;*&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplaceFullPath
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replaceFullPath: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: mock-openai-backend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="generate-test-traffic">Generate Test Traffic&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Send test requests to generate observability data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> i in &lt;span class="o">{&lt;/span>1..10&lt;span class="o">}&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> curl -X POST http://localhost:8080/openai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Content-Type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer test-token&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Test message &amp;#39;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">i&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s1">&amp;#39; for observability demo&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> sleep &lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="viewing-observability-data">Viewing Observability Data&lt;/h2>
&lt;h3 id="in-grafana">In Grafana&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Dashboard Overview&lt;/strong>: Navigate to the agentgateway dashboard&lt;/li>
&lt;li>&lt;strong>Request Metrics&lt;/strong>: See request rates, response times&lt;/li>
&lt;li>&lt;strong>Token Usage&lt;/strong>: Monitor input/output tokens and costs&lt;/li>
&lt;li>&lt;strong>Error Analysis&lt;/strong>: Check error rates and types&lt;/li>
&lt;/ol>
&lt;h3 id="in-tempo-distributed-tracing">In Tempo (Distributed Tracing)&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Go to Explore&lt;/strong> in Grafana&lt;/li>
&lt;li>&lt;strong>Select Tempo&lt;/strong> datasource&lt;/li>
&lt;li>&lt;strong>Search for traces&lt;/strong> by service name: &lt;code>agentgateway&lt;/code>&lt;/li>
&lt;li>&lt;strong>Analyze trace spans&lt;/strong> showing request flow&lt;/li>
&lt;/ol>
&lt;h3 id="key-traces-to-look-for">Key Traces to Look For&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>HTTP Request Span&lt;/strong>: Gateway ingress&lt;/li>
&lt;li>&lt;strong>LLM Provider Span&lt;/strong>: Backend communication&lt;/li>
&lt;li>&lt;strong>Auth Span&lt;/strong>: Authentication processing&lt;/li>
&lt;li>&lt;strong>Rate Limit Span&lt;/strong>: Rate limiting decisions&lt;/li>
&lt;/ul>
&lt;h3 id="sample-tempo-query">Sample Tempo Query&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">{service.name=&amp;#34;agentgateway&amp;#34;}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="advanced-monitoring-configuration">Advanced Monitoring Configuration&lt;/h2>
&lt;h3 id="custom-metrics">Custom Metrics&lt;/h3>
&lt;p>Add custom metrics to track specific business KPIs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">rawConfig&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">config&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">customMetrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;user_requests_total&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;counter&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">help&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Total requests per user&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">user&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;jwt.sub // &amp;#34;anonymous&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.requestModel&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;token_cost_dollars&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;histogram&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">help&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Cost in dollars per request&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">buckets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="m">0.001&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.01&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.1&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10.0&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;llm.totalCost&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="alert-rules">Alert Rules&lt;/h3>
&lt;p>Create Prometheus alert rules for critical conditions:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">groups&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">agentgateway.rules&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HighErrorRate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rate(agentgateway_requests_total{status=~&amp;#34;5..&amp;#34;}[5m]) &amp;gt; 0.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;High error rate detected&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HighLatency&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">histogram_quantile(0.95, rate(agentgateway_request_duration_seconds_bucket[5m])) &amp;gt; 5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;High latency detected&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">HighCost&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">increase(agentgateway_token_cost_dollars_total[1h]) &amp;gt; 50&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">0m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;High hourly cost detected&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="monitoring-best-practices">Monitoring Best Practices&lt;/h2>
&lt;h3 id="resource-planning">Resource Planning&lt;/h3>
&lt;p>Monitor these key metrics for capacity planning:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Request Rate&lt;/strong>: Track requests/second trends&lt;/li>
&lt;li>&lt;strong>Token Velocity&lt;/strong>: Monitor tokens/minute per model&lt;/li>
&lt;li>&lt;strong>Cost Burn Rate&lt;/strong>: Track $/hour consumption&lt;/li>
&lt;li>&lt;strong>Provider Latency&lt;/strong>: Monitor P95/P99 response times&lt;/li>
&lt;/ol>
&lt;h3 id="performance-optimization">Performance Optimization&lt;/h3>
&lt;p>Use observability data to optimize:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Route Configuration&lt;/strong>: Based on latency patterns&lt;/li>
&lt;li>&lt;strong>Caching Strategies&lt;/strong>: Based on request patterns&lt;/li>
&lt;li>&lt;strong>Rate Limiting&lt;/strong>: Based on usage distribution&lt;/li>
&lt;li>&lt;strong>Provider Selection&lt;/strong>: Based on cost/performance&lt;/li>
&lt;/ol>
&lt;h3 id="cost-management">Cost Management&lt;/h3>
&lt;p>Track and alert on:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Daily/Monthly spend&lt;/strong> per provider&lt;/li>
&lt;li>&lt;strong>Cost per user/team&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Token efficiency&lt;/strong> (output/input ratio)&lt;/li>
&lt;li>&lt;strong>Most expensive models/users&lt;/strong>&lt;/li>
&lt;/ol>
&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;h3 id="missing-traces">Missing Traces&lt;/h3>
&lt;p>If traces aren&amp;rsquo;t appearing in Tempo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Tempo is receiving traces&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>tempo -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify agentgateway trace configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get enterpriseagentgatewayparameters agentgateway-params -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check connectivity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n enterprise-agentgateway deployment/agentgateway -- nc -zv tempo.monitoring.svc.cluster.local &lt;span class="m">4317&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="missing-metrics">Missing Metrics&lt;/h3>
&lt;p>If metrics aren&amp;rsquo;t showing in Prometheus:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Prometheus targets&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://localhost:30090/targets
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify agentgateway metrics endpoint&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl http://localhost:30091/metrics
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Prometheus configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get configmap prometheus-server -n monitoring -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="dashboard-issues">Dashboard Issues&lt;/h3>
&lt;p>If the dashboard isn&amp;rsquo;t loading:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check Grafana pod logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>grafana -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify datasource connectivity&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n monitoring deployment/grafana -- nc -zv prometheus-server &lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n monitoring deployment/grafana -- nc -zv tempo &lt;span class="m">3100&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>To remove the monitoring stack:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm uninstall grafana -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall prometheus -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall tempo -n monitoring
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete namespace monitoring
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>With comprehensive observability in place, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Set up development environments&lt;/strong> with mock providers&lt;/li>
&lt;li>&lt;strong>Configure real AI provider integrations&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Implement advanced routing strategies&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Add security and rate limiting policies&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll create a mock OpenAI environment for cost-free development and testing.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Complete visibility&lt;/strong> into AI Gateway performance and costs&lt;/li>
&lt;li>&lt;strong>Real-time monitoring&lt;/strong> with Grafana dashboards&lt;/li>
&lt;li>&lt;strong>Distributed tracing&lt;/strong> for request flow analysis&lt;/li>
&lt;li>&lt;strong>Cost tracking&lt;/strong> and optimization insights&lt;/li>
&lt;li>&lt;strong>Production-ready&lt;/strong> monitoring stack for kind clusters&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway is now fully instrumented and ready for production workloads with comprehensive observability!&lt;/p></content:encoded></item><item><title>Setting Up Enterprise agentgateway on Kind Clusters</title><link>https://maniak.io/articles/01-setting-up-enterprise-agentgateway-on-kind-clusters/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/01-setting-up-enterprise-agentgateway-on-kind-clusters/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Enterprise agentgateway by Solo.io is a powerful AI Gateway solution that provides unified access to multiple LLM providers with advanced features like routing, security, observability, and cost management. In this comprehensive guide, we&amp;rsquo;ll walk you through setting up Enterprise agentgateway on a kind (Kubernetes in Docker) cluster - perfect for development, testing, and learning environments.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>How to set up a kind cluster optimized for agentgateway&lt;/li>
&lt;li>Installing Kubernetes Gateway API and agentgateway CRDs&lt;/li>
&lt;li>Deploying the Enterprise agentgateway controller&lt;/li>
&lt;li>Configuring your first agentgateway instance&lt;/li>
&lt;li>Validating your installation&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>Before starting this guide, ensure you have:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Solo.io Trial License Key&lt;/strong>: Get a free trial at &lt;a href="https://www.solo.io/">Solo.io&lt;/a>&lt;/li>
&lt;li>&lt;strong>Docker Desktop&lt;/strong> or Docker Engine running&lt;/li>
&lt;li>&lt;strong>kind CLI&lt;/strong> installed (&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/">installation guide&lt;/a>)&lt;/li>
&lt;li>&lt;strong>kubectl CLI&lt;/strong> installed and configured&lt;/li>
&lt;li>&lt;strong>helm CLI&lt;/strong> installed (version 3.x)&lt;/li>
&lt;/ul>
&lt;h2 id="kind-cluster-setup">Kind Cluster Setup&lt;/h2>
&lt;h3 id="create-kind-cluster-configuration">Create Kind Cluster Configuration&lt;/h3>
&lt;p>First, let&amp;rsquo;s create a kind cluster with the proper configuration for running agentgateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF &amp;gt; kind-agentgateway-cluster.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Cluster
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: kind.x-k8s.io/v1alpha4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">nodes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">- role: control-plane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubeadmConfigPatches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: InitConfiguration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodeRegistration:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubeletExtraArgs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> node-labels: &amp;#34;ingress-ready=true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extraPortMappings:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostPort: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 443
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostPort: 443
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">- role: worker
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">- role: worker
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-cluster">Create the Cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the kind cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind create cluster --config kind-agentgateway-cluster.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify the cluster is ready&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cluster-info --context kind-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME STATUS ROLES AGE VERSION
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-control-plane Ready control-plane 2m v1.29.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-worker Ready &amp;lt;none&amp;gt; 90s v1.29.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-worker2 Ready &amp;lt;none&amp;gt; 90s v1.29.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="environment-setup">Environment Setup&lt;/h2>
&lt;h3 id="configure-required-variables">Configure Required Variables&lt;/h3>
&lt;p>Export your Solo.io trial license key and agentgateway version:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Replace with your actual license key from Solo.io&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-license-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ENTERPRISE_AGW_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;2.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify the variables are set&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;License Key: &lt;/span>&lt;span class="nv">$SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;agentgateway Version: &lt;/span>&lt;span class="nv">$ENTERPRISE_AGW_VERSION&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="installing-kubernetes-gateway-api">Installing Kubernetes Gateway API&lt;/h2>
&lt;p>agentgateway builds on the Kubernetes Gateway API. We&amp;rsquo;ll use the experimental CRDs to enable advanced features:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install Kubernetes Gateway API CRDs (experimental for advanced features)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for CRDs to be established&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/gatewayclasses.gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/gateways.gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/httproutes.gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-gateway-api-installation">Verify Gateway API Installation&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl api-resources --api-group&lt;span class="o">=&lt;/span>gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME SHORTNAMES APIVERSION NAMESPACED KIND
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">backendtlspolicies btlspolicy gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> BackendTLSPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gatewayclasses gc gateway.networking.k8s.io/v1 &lt;span class="nb">false&lt;/span> GatewayClass
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gateways gtw gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> Gateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcroutes gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> GRPCRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">httproutes gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> HTTPRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">referencegrants refgrant gateway.networking.k8s.io/v1beta1 &lt;span class="nb">true&lt;/span> ReferenceGrant
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tcproutes gateway.networking.k8s.io/v1alpha2 &lt;span class="nb">true&lt;/span> TCPRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tlsroutes gateway.networking.k8s.io/v1alpha3 &lt;span class="nb">true&lt;/span> TLSRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">udproutes gateway.networking.k8s.io/v1alpha2 &lt;span class="nb">true&lt;/span> UDPRoute
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="installing-enterprise-agentgateway">Installing Enterprise agentgateway&lt;/h2>
&lt;h3 id="create-namespace-and-install-crds">Create Namespace and Install CRDs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create namespace for agentgateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create namespace enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install Enterprise agentgateway CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace --namespace enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$ENTERPRISE_AGW_VERSION&lt;/span> enterprise-agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts/enterprise-agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for CRDs to be established&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/agentgatewaybackends.agentgateway.dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/agentgatewayparameters.agentgateway.dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/agentgatewaypolicies.agentgateway.dev
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-agentgateway-crds">Verify agentgateway CRDs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl api-resources &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;NR==1 || /enterpriseagentgateway\\.solo\\.io|agentgateway\\.dev|ratelimit\\.solo\\.io|extauth\\.solo\\.io/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME SHORTNAMES APIVERSION NAMESPACED KIND
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgatewaybackends agbe agentgateway.dev/v1alpha1 &lt;span class="nb">true&lt;/span> AgentgatewayBackend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgatewayparameters agpar agentgateway.dev/v1alpha1 &lt;span class="nb">true&lt;/span> AgentgatewayParameters
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgatewaypolicies agpol agentgateway.dev/v1alpha1 &lt;span class="nb">true&lt;/span> AgentgatewayPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterpriseagentgatewayparameters eagpar enterpriseagentgateway.solo.io/v1alpha1 &lt;span class="nb">true&lt;/span> EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterpriseagentgatewaypolicies eagpol enterpriseagentgateway.solo.io/v1alpha1 &lt;span class="nb">true&lt;/span> EnterpriseAgentgatewayPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">authconfigs ac extauth.solo.io/v1 &lt;span class="nb">true&lt;/span> AuthConfig
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ratelimitconfigs rlc ratelimit.solo.io/v1alpha1 &lt;span class="nb">true&lt;/span> RateLimitConfig
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="install-enterprise-agentgateway-controller">Install Enterprise agentgateway Controller&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i -n enterprise-agentgateway enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts/enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$ENTERPRISE_AGW_VERSION&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string licensing.licenseKey&lt;span class="o">=&lt;/span>&lt;span class="nv">$SOLO_TRIAL_LICENSE_KEY&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f -&lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Gateway Class parameters reference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">gatewayClassParametersRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enterprise-agentgateway:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: enterpriseagentgateway.solo.io
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-params
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-controller-installation">Verify Controller Installation&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check that the controller is running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for the controller to be ready&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Ready pod -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>enterprise-agentgateway -n enterprise-agentgateway --timeout&lt;span class="o">=&lt;/span>300s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterprise-agentgateway-5fc9d95758-n8vvb 1/1 Running &lt;span class="m">0&lt;/span> 87s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="deploy-agentgateway-instance">Deploy agentgateway Instance&lt;/h2>
&lt;p>Now let&amp;rsquo;s create an agentgateway instance with optimized configuration for kind clusters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: enterpriseagentgateway.solo.io/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-params
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Enable shared extensions for auth, rate limiting, and caching
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sharedExtensions:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extauth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ratelimiter:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extCache:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure logging
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> logging:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> level: info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure service for kind cluster (NodePort for local access)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Observability configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rawConfig:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> logging:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body: json(request.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: json(response.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body.modelId: json(request.body).modelId
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> format: json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tracing:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> randomSampling: &amp;#39;true&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.operation.name: &amp;#39;&amp;#34;chat&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.system: &amp;#34;llm.provider&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.prompt: &amp;#39;llm.prompt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.completion: &amp;#39;llm.completion.map(c, {&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;, &amp;#34;content&amp;#34;: c})&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request.model: &amp;#34;llm.requestModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.response.model: &amp;#34;llm.responseModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.completion_tokens: &amp;#34;llm.outputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.prompt_tokens: &amp;#34;llm.inputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request: &amp;#39;flatten(llm.params)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: &amp;#39;json(response.body)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Deployment configuration optimized for kind
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1 # Single replica for development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> resources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> requests:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 200m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 128Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> limits:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 500m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 256Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> allowedRoutes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespaces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from: All
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="validation">Validation&lt;/h2>
&lt;h3 id="check-all-pods-are-running">Check All Pods Are Running&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-7d4c8c4d4b-lvdsq 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterprise-agentgateway-5f9c5b95b4-gjblt 1/1 Running &lt;span class="m">0&lt;/span> 5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ext-auth-service-enterprise-agentgateway-6fcc5bc989-22wgd 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ext-cache-enterprise-agentgateway-6bfcb8c87d-vjzxn 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rate-limiter-enterprise-agentgateway-589f66bb88-xz7nm 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="check-gateway-status">Check Gateway Status&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get gateway agentgateway -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Look for the status section showing the gateway is accepted and programmed.&lt;/p>
&lt;h3 id="test-gateway-accessibility">Test Gateway Accessibility&lt;/h3>
&lt;p>For kind clusters, we can access the gateway through the NodePort:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Get the kind cluster&amp;#39;s node port&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n enterprise-agentgateway --selector&lt;span class="o">=&lt;/span>gateway.networking.k8s.io/gateway-name&lt;span class="o">=&lt;/span>agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test basic connectivity (should return 404 since no routes are configured yet)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -v http://localhost:8080/test
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected response: HTTP 404 (which is correct - no routes configured yet)&lt;/p>
&lt;h2 id="understanding-your-setup">Understanding Your Setup&lt;/h2>
&lt;h3 id="what-weve-deployed">What We&amp;rsquo;ve Deployed&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Kubernetes Gateway API&lt;/strong>: Standard APIs for traffic management&lt;/li>
&lt;li>&lt;strong>Enterprise agentgateway Controller&lt;/strong>: Manages agentgateway lifecycle&lt;/li>
&lt;li>&lt;strong>agentgateway Data Plane&lt;/strong>: Routes traffic to AI providers&lt;/li>
&lt;li>&lt;strong>Shared Extensions&lt;/strong>:
&lt;ul>
&lt;li>&lt;strong>ext-auth-service&lt;/strong>: Handles authentication (JWT, API keys)&lt;/li>
&lt;li>&lt;strong>rate-limiter&lt;/strong>: Manages rate limiting and quotas&lt;/li>
&lt;li>&lt;strong>ext-cache&lt;/strong>: Provides caching capabilities&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="network-configuration-for-kind">Network Configuration for Kind&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Gateway Service&lt;/strong>: NodePort on port 30080 for local access&lt;/li>
&lt;li>&lt;strong>Internal Port&lt;/strong>: 8080 for cluster-internal communication&lt;/li>
&lt;li>&lt;strong>Host Access&lt;/strong>: &lt;code>http://localhost:8080&lt;/code> from your development machine&lt;/li>
&lt;/ul>
&lt;h2 id="environment-setup-script">Environment Setup Script&lt;/h2>
&lt;p>Create a helper script for managing your agentgateway environment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; agentgateway-env.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># agentgateway Environment Setup Script
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">export SOLO_TRIAL_LICENSE_KEY=&amp;#34;${SOLO_TRIAL_LICENSE_KEY:-your-license-key-here}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">export ENTERPRISE_AGW_VERSION=&amp;#34;2.1.0&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Helper functions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">agw_status() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== agentgateway Status ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get gateway agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">agw_logs() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail=50 -f
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">agw_reset() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Resetting agentgateway...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete namespace enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Re-run installation commands
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;agentgateway environment loaded!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Available commands: agw_status, agw_logs, agw_reset&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Make it executable and source it&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x agentgateway-env.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">source&lt;/span> agentgateway-env.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;h3 id="common-issues">Common Issues&lt;/h3>
&lt;p>&lt;strong>1. License Key Issues&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check controller logs for license errors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/enterprise-agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. CRD Installation Problems&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Reinstall CRDs if needed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete crd -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>enterprise-agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Then re-run the helm install command&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. Pod Startup Issues&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check pod events&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe pod -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check resource constraints&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl top pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>4. Gateway Not Ready&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check gateway status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe gateway agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify GatewayClass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gatewayclass enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done testing, clean up your environment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the agentgateway installation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall enterprise-agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall enterprise-agentgateway-crds -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the namespace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete namespace enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the kind cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>Now that you have agentgateway running on your kind cluster, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Set up monitoring and observability&lt;/strong> - Install Grafana, Prometheus, and Tempo for comprehensive observability&lt;/li>
&lt;li>&lt;strong>Configure your first AI route&lt;/strong> - Connect to OpenAI, Anthropic, or other providers&lt;/li>
&lt;li>&lt;strong>Explore advanced features&lt;/strong> - Security, rate limiting, and traffic management&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll set up a complete observability stack to monitor your agentgateway&amp;rsquo;s performance, costs, and usage patterns.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>Enterprise agentgateway provides a unified interface for multiple AI providers&lt;/li>
&lt;li>Kind clusters are perfect for development and testing agentgateway&lt;/li>
&lt;li>The setup includes controller, data plane, and essential shared extensions&lt;/li>
&lt;li>Proper observability configuration enables comprehensive monitoring&lt;/li>
&lt;li>NodePort service configuration allows easy local access in kind environments&lt;/li>
&lt;/ul>
&lt;p>With your agentgateway foundation in place, you&amp;rsquo;re ready to build sophisticated AI routing and management capabilities!&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Enterprise agentgateway by Solo.io is a powerful AI Gateway solution that provides unified access to multiple LLM providers with advanced features like routing, security, observability, and cost management. In this comprehensive guide, we&amp;rsquo;ll walk you through setting up Enterprise agentgateway on a kind (Kubernetes in Docker) cluster - perfect for development, testing, and learning environments.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>How to set up a kind cluster optimized for agentgateway&lt;/li>
&lt;li>Installing Kubernetes Gateway API and agentgateway CRDs&lt;/li>
&lt;li>Deploying the Enterprise agentgateway controller&lt;/li>
&lt;li>Configuring your first agentgateway instance&lt;/li>
&lt;li>Validating your installation&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;p>Before starting this guide, ensure you have:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Solo.io Trial License Key&lt;/strong>: Get a free trial at &lt;a href="https://www.solo.io/">Solo.io&lt;/a>&lt;/li>
&lt;li>&lt;strong>Docker Desktop&lt;/strong> or Docker Engine running&lt;/li>
&lt;li>&lt;strong>kind CLI&lt;/strong> installed (&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/">installation guide&lt;/a>)&lt;/li>
&lt;li>&lt;strong>kubectl CLI&lt;/strong> installed and configured&lt;/li>
&lt;li>&lt;strong>helm CLI&lt;/strong> installed (version 3.x)&lt;/li>
&lt;/ul>
&lt;h2 id="kind-cluster-setup">Kind Cluster Setup&lt;/h2>
&lt;h3 id="create-kind-cluster-configuration">Create Kind Cluster Configuration&lt;/h3>
&lt;p>First, let&amp;rsquo;s create a kind cluster with the proper configuration for running agentgateway:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;EOF &amp;gt; kind-agentgateway-cluster.yaml
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Cluster
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: kind.x-k8s.io/v1alpha4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">nodes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">- role: control-plane
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubeadmConfigPatches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - |
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: InitConfiguration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodeRegistration:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubeletExtraArgs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> node-labels: &amp;#34;ingress-ready=true&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extraPortMappings:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostPort: 80
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 443
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostPort: 443
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - containerPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> hostPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">- role: worker
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">- role: worker
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-cluster">Create the Cluster&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create the kind cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind create cluster --config kind-agentgateway-cluster.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify the cluster is ready&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cluster-info --context kind-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get nodes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME STATUS ROLES AGE VERSION
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-control-plane Ready control-plane 2m v1.29.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-worker Ready &amp;lt;none&amp;gt; 90s v1.29.1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-worker2 Ready &amp;lt;none&amp;gt; 90s v1.29.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="environment-setup">Environment Setup&lt;/h2>
&lt;h3 id="configure-required-variables">Configure Required Variables&lt;/h3>
&lt;p>Export your Solo.io trial license key and agentgateway version:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Replace with your actual license key from Solo.io&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-license-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ENTERPRISE_AGW_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;2.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify the variables are set&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;License Key: &lt;/span>&lt;span class="nv">$SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;agentgateway Version: &lt;/span>&lt;span class="nv">$ENTERPRISE_AGW_VERSION&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="installing-kubernetes-gateway-api">Installing Kubernetes Gateway API&lt;/h2>
&lt;p>agentgateway builds on the Kubernetes Gateway API. We&amp;rsquo;ll use the experimental CRDs to enable advanced features:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install Kubernetes Gateway API CRDs (experimental for advanced features)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for CRDs to be established&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/gatewayclasses.gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/gateways.gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/httproutes.gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-gateway-api-installation">Verify Gateway API Installation&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl api-resources --api-group&lt;span class="o">=&lt;/span>gateway.networking.k8s.io
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME SHORTNAMES APIVERSION NAMESPACED KIND
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">backendtlspolicies btlspolicy gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> BackendTLSPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gatewayclasses gc gateway.networking.k8s.io/v1 &lt;span class="nb">false&lt;/span> GatewayClass
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">gateways gtw gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> Gateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">grpcroutes gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> GRPCRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">httproutes gateway.networking.k8s.io/v1 &lt;span class="nb">true&lt;/span> HTTPRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">referencegrants refgrant gateway.networking.k8s.io/v1beta1 &lt;span class="nb">true&lt;/span> ReferenceGrant
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tcproutes gateway.networking.k8s.io/v1alpha2 &lt;span class="nb">true&lt;/span> TCPRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tlsroutes gateway.networking.k8s.io/v1alpha3 &lt;span class="nb">true&lt;/span> TLSRoute
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">udproutes gateway.networking.k8s.io/v1alpha2 &lt;span class="nb">true&lt;/span> UDPRoute
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="installing-enterprise-agentgateway">Installing Enterprise agentgateway&lt;/h2>
&lt;h3 id="create-namespace-and-install-crds">Create Namespace and Install CRDs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create namespace for agentgateway&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create namespace enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Install Enterprise agentgateway CRDs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm upgrade -i --create-namespace --namespace enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$ENTERPRISE_AGW_VERSION&lt;/span> enterprise-agentgateway-crds &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts/enterprise-agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for CRDs to be established&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/agentgatewaybackends.agentgateway.dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/agentgatewayparameters.agentgateway.dev
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for &lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Established crd/agentgatewaypolicies.agentgateway.dev
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-agentgateway-crds">Verify agentgateway CRDs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl api-resources &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;NR==1 || /enterpriseagentgateway\\.solo\\.io|agentgateway\\.dev|ratelimit\\.solo\\.io|extauth\\.solo\\.io/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME SHORTNAMES APIVERSION NAMESPACED KIND
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgatewaybackends agbe agentgateway.dev/v1alpha1 &lt;span class="nb">true&lt;/span> AgentgatewayBackend
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgatewayparameters agpar agentgateway.dev/v1alpha1 &lt;span class="nb">true&lt;/span> AgentgatewayParameters
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgatewaypolicies agpol agentgateway.dev/v1alpha1 &lt;span class="nb">true&lt;/span> AgentgatewayPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterpriseagentgatewayparameters eagpar enterpriseagentgateway.solo.io/v1alpha1 &lt;span class="nb">true&lt;/span> EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterpriseagentgatewaypolicies eagpol enterpriseagentgateway.solo.io/v1alpha1 &lt;span class="nb">true&lt;/span> EnterpriseAgentgatewayPolicy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">authconfigs ac extauth.solo.io/v1 &lt;span class="nb">true&lt;/span> AuthConfig
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ratelimitconfigs rlc ratelimit.solo.io/v1alpha1 &lt;span class="nb">true&lt;/span> RateLimitConfig
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="install-enterprise-agentgateway-controller">Install Enterprise agentgateway Controller&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade -i -n enterprise-agentgateway enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> oci://us-docker.pkg.dev/solo-public/enterprise-agentgateway/charts/enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --create-namespace &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --version &lt;span class="nv">$ENTERPRISE_AGW_VERSION&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set-string licensing.licenseKey&lt;span class="o">=&lt;/span>&lt;span class="nv">$SOLO_TRIAL_LICENSE_KEY&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -f -&lt;span class="s">&amp;lt;&amp;lt;EOF
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Gateway Class parameters reference
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">gatewayClassParametersRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enterprise-agentgateway:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: enterpriseagentgateway.solo.io
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-params
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-controller-installation">Verify Controller Installation&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check that the controller is running&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Wait for the controller to be ready&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>Ready pod -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>enterprise-agentgateway -n enterprise-agentgateway --timeout&lt;span class="o">=&lt;/span>300s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterprise-agentgateway-5fc9d95758-n8vvb 1/1 Running &lt;span class="m">0&lt;/span> 87s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="deploy-agentgateway-instance">Deploy agentgateway Instance&lt;/h2>
&lt;p>Now let&amp;rsquo;s create an agentgateway instance with optimized configuration for kind clusters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: enterpriseagentgateway.solo.io/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: EnterpriseAgentgatewayParameters
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway-params
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Enable shared extensions for auth, rate limiting, and caching
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sharedExtensions:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extauth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ratelimiter:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> extCache:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> enabled: true
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure logging
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> logging:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> level: info
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure service for kind cluster (NodePort for local access)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> service:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: NodePort
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ports:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> targetPort: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> nodePort: 30080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: TCP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Observability configuration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rawConfig:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> logging:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body: json(request.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: json(response.body)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request.body.modelId: json(request.body).modelId
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> format: json
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> tracing:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> randomSampling: &amp;#39;true&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fields:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> add:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.operation.name: &amp;#39;&amp;#34;chat&amp;#34;&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.system: &amp;#34;llm.provider&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.prompt: &amp;#39;llm.prompt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.completion: &amp;#39;llm.completion.map(c, {&amp;#34;role&amp;#34;:&amp;#34;assistant&amp;#34;, &amp;#34;content&amp;#34;: c})&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request.model: &amp;#34;llm.requestModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.response.model: &amp;#34;llm.responseModel&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.completion_tokens: &amp;#34;llm.outputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.usage.prompt_tokens: &amp;#34;llm.inputTokens&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gen_ai.request: &amp;#39;flatten(llm.params)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jwt: &amp;#39;jwt&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> response.body: &amp;#39;json(response.body)&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Deployment configuration optimized for kind
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> deployment:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replicas: 1 # Single replica for development
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> template:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> containers:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> resources:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> requests:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 200m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 128Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> limits:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cpu: 500m
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> memory: 256Mi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: Gateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gatewayClassName: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> listeners:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: http
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> port: 8080
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> protocol: HTTP
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> allowedRoutes:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespaces:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> from: All
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="validation">Validation&lt;/h2>
&lt;h3 id="check-all-pods-are-running">Check All Pods Are Running&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">NAME READY STATUS RESTARTS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">agentgateway-7d4c8c4d4b-lvdsq 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">enterprise-agentgateway-5f9c5b95b4-gjblt 1/1 Running &lt;span class="m">0&lt;/span> 5m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ext-auth-service-enterprise-agentgateway-6fcc5bc989-22wgd 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ext-cache-enterprise-agentgateway-6bfcb8c87d-vjzxn 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rate-limiter-enterprise-agentgateway-589f66bb88-xz7nm 1/1 Running &lt;span class="m">0&lt;/span> 2m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="check-gateway-status">Check Gateway Status&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get gateway agentgateway -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Look for the status section showing the gateway is accepted and programmed.&lt;/p>
&lt;h3 id="test-gateway-accessibility">Test Gateway Accessibility&lt;/h3>
&lt;p>For kind clusters, we can access the gateway through the NodePort:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Get the kind cluster&amp;#39;s node port&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n enterprise-agentgateway --selector&lt;span class="o">=&lt;/span>gateway.networking.k8s.io/gateway-name&lt;span class="o">=&lt;/span>agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test basic connectivity (should return 404 since no routes are configured yet)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -v http://localhost:8080/test
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected response: HTTP 404 (which is correct - no routes configured yet)&lt;/p>
&lt;h2 id="understanding-your-setup">Understanding Your Setup&lt;/h2>
&lt;h3 id="what-weve-deployed">What We&amp;rsquo;ve Deployed&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Kubernetes Gateway API&lt;/strong>: Standard APIs for traffic management&lt;/li>
&lt;li>&lt;strong>Enterprise agentgateway Controller&lt;/strong>: Manages agentgateway lifecycle&lt;/li>
&lt;li>&lt;strong>agentgateway Data Plane&lt;/strong>: Routes traffic to AI providers&lt;/li>
&lt;li>&lt;strong>Shared Extensions&lt;/strong>:
&lt;ul>
&lt;li>&lt;strong>ext-auth-service&lt;/strong>: Handles authentication (JWT, API keys)&lt;/li>
&lt;li>&lt;strong>rate-limiter&lt;/strong>: Manages rate limiting and quotas&lt;/li>
&lt;li>&lt;strong>ext-cache&lt;/strong>: Provides caching capabilities&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="network-configuration-for-kind">Network Configuration for Kind&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Gateway Service&lt;/strong>: NodePort on port 30080 for local access&lt;/li>
&lt;li>&lt;strong>Internal Port&lt;/strong>: 8080 for cluster-internal communication&lt;/li>
&lt;li>&lt;strong>Host Access&lt;/strong>: &lt;code>http://localhost:8080&lt;/code> from your development machine&lt;/li>
&lt;/ul>
&lt;h2 id="environment-setup-script">Environment Setup Script&lt;/h2>
&lt;p>Create a helper script for managing your agentgateway environment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; agentgateway-env.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># agentgateway Environment Setup Script
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">export SOLO_TRIAL_LICENSE_KEY=&amp;#34;${SOLO_TRIAL_LICENSE_KEY:-your-license-key-here}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">export ENTERPRISE_AGW_VERSION=&amp;#34;2.1.0&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Helper functions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">agw_status() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== agentgateway Status ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get gateway agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">agw_logs() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail=50 -f
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">agw_reset() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Resetting agentgateway...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete namespace enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Re-run installation commands
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;agentgateway environment loaded!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Available commands: agw_status, agw_logs, agw_reset&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Make it executable and source it&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x agentgateway-env.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">source&lt;/span> agentgateway-env.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="troubleshooting">Troubleshooting&lt;/h2>
&lt;h3 id="common-issues">Common Issues&lt;/h3>
&lt;p>&lt;strong>1. License Key Issues&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check controller logs for license errors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/enterprise-agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. CRD Installation Problems&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Reinstall CRDs if needed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete crd -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>enterprise-agentgateway-crds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Then re-run the helm install command&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. Pod Startup Issues&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check pod events&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe pod -l app.kubernetes.io/name&lt;span class="o">=&lt;/span>agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check resource constraints&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl top pods -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>4. Gateway Not Ready&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check gateway status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe gateway agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify GatewayClass&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get gatewayclass enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cleanup">Cleanup&lt;/h2>
&lt;p>When you&amp;rsquo;re done testing, clean up your environment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the agentgateway installation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall enterprise-agentgateway -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm uninstall enterprise-agentgateway-crds -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the namespace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl delete namespace enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete the kind cluster&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kind delete cluster --name agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>Now that you have agentgateway running on your kind cluster, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Set up monitoring and observability&lt;/strong> - Install Grafana, Prometheus, and Tempo for comprehensive observability&lt;/li>
&lt;li>&lt;strong>Configure your first AI route&lt;/strong> - Connect to OpenAI, Anthropic, or other providers&lt;/li>
&lt;li>&lt;strong>Explore advanced features&lt;/strong> - Security, rate limiting, and traffic management&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll set up a complete observability stack to monitor your agentgateway&amp;rsquo;s performance, costs, and usage patterns.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>Enterprise agentgateway provides a unified interface for multiple AI providers&lt;/li>
&lt;li>Kind clusters are perfect for development and testing agentgateway&lt;/li>
&lt;li>The setup includes controller, data plane, and essential shared extensions&lt;/li>
&lt;li>Proper observability configuration enables comprehensive monitoring&lt;/li>
&lt;li>NodePort service configuration allows easy local access in kind environments&lt;/li>
&lt;/ul>
&lt;p>With your agentgateway foundation in place, you&amp;rsquo;re ready to build sophisticated AI routing and management capabilities!&lt;/p></content:encoded></item><item><title>Your First AI Route: Connecting to OpenAI with agentgateway</title><link>https://maniak.io/articles/04-your-first-ai-route-connecting-to-openai/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/04-your-first-ai-route-connecting-to-openai/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Now that you have agentgateway running with observability and have tested with a mock environment, it&amp;rsquo;s time to connect to a real AI provider. OpenAI is the perfect starting point due to its widespread adoption, comprehensive API, and excellent documentation.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll create your first production-ready route to OpenAI, implement proper security practices, and demonstrate the observability benefits of routing AI traffic through agentgateway.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Create secure API key storage for OpenAI&lt;/li>
&lt;li>Configure an AgentgatewayBackend for OpenAI&lt;/li>
&lt;li>Set up HTTPRoutes for different OpenAI endpoints&lt;/li>
&lt;li>Test chat completions, embeddings, and model listings&lt;/li>
&lt;li>Monitor real AI requests through Grafana&lt;/li>
&lt;li>Understand token usage and cost tracking&lt;/li>
&lt;li>Compare mock vs real provider behavior&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Completed previous blog posts (agentgateway setup, observability, mock environment)&lt;/li>
&lt;li>&lt;strong>Valid OpenAI API Key&lt;/strong> with credits (get from &lt;a href="https://platform.openai.com">OpenAI Platform&lt;/a>)&lt;/li>
&lt;li>Kind cluster with agentgateway and monitoring running&lt;/li>
&lt;li>Basic understanding of OpenAI API structure&lt;/li>
&lt;/ul>
&lt;h2 id="environment-setup">Environment Setup&lt;/h2>
&lt;h3 id="prepare-your-openai-api-key">Prepare Your OpenAI API Key&lt;/h3>
&lt;p>First, ensure you have a valid OpenAI API key with appropriate permissions:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Visit OpenAI Platform&lt;/strong>: Go to &lt;a href="https://platform.openai.com">platform.openai.com&lt;/a>&lt;/li>
&lt;li>&lt;strong>Create API Key&lt;/strong>: Navigate to API Keys section and create a new key&lt;/li>
&lt;li>&lt;strong>Set Usage Limits&lt;/strong>: Configure spending limits to control costs&lt;/li>
&lt;li>&lt;strong>Note the Key&lt;/strong>: Copy your API key securely&lt;/li>
&lt;/ol>
&lt;h3 id="set-environment-variables">Set Environment Variables&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set your OpenAI API key (replace with your actual key)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-your-openai-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify other environment variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-license-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ENTERPRISE_AGW_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;2.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify the key is set correctly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OpenAI API Key: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nv">0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nv">10&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test the key directly (optional)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0:3] | .[].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="creating-secure-api-key-storage">Creating Secure API Key Storage&lt;/h2>
&lt;h3 id="create-kubernetes-secret">Create Kubernetes Secret&lt;/h3>
&lt;p>Store your OpenAI API key securely in a Kubernetes secret:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create secret with proper authorization header format&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-secret &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Authorization=Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dry-run&lt;span class="o">=&lt;/span>client -o yaml &lt;span class="p">|&lt;/span> kubectl apply -f -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret creation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># View secret structure (without revealing the key)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n enterprise-agentgateway -o yaml &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sed &lt;span class="s1">&amp;#39;s/Authorization:.*/Authorization: [REDACTED]/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="security-best-practices">Security Best Practices&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Label the secret for better organization&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl label secret openai-secret -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">provider&lt;/span>&lt;span class="o">=&lt;/span>openai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">purpose&lt;/span>&lt;span class="o">=&lt;/span>api-authentication
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret permissions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl auth can-i get secrets -n enterprise-agentgateway --as&lt;span class="o">=&lt;/span>system:serviceaccount:enterprise-agentgateway:default
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="configuring-openai-backend">Configuring OpenAI Backend&lt;/h2>
&lt;h3 id="create-agentgatewaybackend">Create AgentgatewayBackend&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> environment: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure OpenAI as the AI provider
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Use default OpenAI endpoint (api.openai.com)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Optionally specify config like:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # endpoint: &amp;#34;https://api.openai.com&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # model: &amp;#34;gpt-4o-mini&amp;#34; # Default model override
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Authentication policy using our secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Key defaults to &amp;#34;Authorization&amp;#34; if not specified
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Optional: Configure timeout and retry policies
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34; # 2 minutes for long-running requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Optional: Add retry policy for resilience
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retry:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> attempts: 3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backoff:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> baseInterval: &amp;#34;1s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> maxInterval: &amp;#34;10s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retryOn:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;5xx&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;reset&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;connect-failure&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;refused-stream&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-backend-configuration">Verify Backend Configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check backend status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Ensure backend is accepted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway -o &lt;span class="nv">jsonpath&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{.status.conditions[?(@.type==&amp;#34;Accepted&amp;#34;)].status}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="creating-openai-routes">Creating OpenAI Routes&lt;/h2>
&lt;h3 id="basic-chat-completions-route">Basic Chat Completions Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: chat-completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Match requests to /openai/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure timeouts for potentially long AI requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Transform the path to OpenAI&amp;#39;s expected format
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="models-and-embeddings-routes">Models and Embeddings Routes&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai/models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;60s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-routes">Verify Routes&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check all OpenAI routes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute -n enterprise-agentgateway -l &lt;span class="nv">provider&lt;/span>&lt;span class="o">=&lt;/span>openai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify route status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute openai-chat -n enterprise-agentgateway -o yaml &lt;span class="p">|&lt;/span> grep -A &lt;span class="m">10&lt;/span> status:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="testing-your-openai-integration">Testing Your OpenAI Integration&lt;/h2>
&lt;h3 id="get-gateway-endpoint">Get Gateway Endpoint&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Get gateway service details&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n enterprise-agentgateway --selector&lt;span class="o">=&lt;/span>gateway.networking.k8s.io/gateway-name&lt;span class="o">=&lt;/span>agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># For kind clusters, use localhost&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;agentgateway available at: &lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-chat-completions">Test Chat Completions&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test basic chat completion&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-request-id: test-&lt;/span>&lt;span class="k">$(&lt;/span>date +%s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;system&amp;#34;, 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;You are a helpful assistant that explains AI Gateway technology.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What are the key benefits of using an AI Gateway like agentgateway for managing LLM requests?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 200,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;temperature&amp;#34;: 0.7
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected response:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chatcmpl-abc123def456&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chat.completion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1701234567&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;gpt-4o-mini-2024-07-18&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;choices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;An AI Gateway like agentgateway provides several key benefits for managing LLM requests:\n\n1. **Unified Access**: Single endpoint for multiple AI providers (OpenAI, Anthropic, AWS Bedrock)\n2. **Observability**: Comprehensive metrics, logging, and tracing of AI requests\n3. **Cost Management**: Token usage tracking and cost attribution across teams\n4. **Security**: Centralized authentication, API key management, and access control\n5. **Rate Limiting**: Protect against excessive usage and manage quotas\n6. **Reliability**: Failover between providers, retry policies, and circuit breakers\n\nThis centralized approach simplifies AI operations while providing enterprise-grade governance.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;finish_reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;stop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;usage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;prompt_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">45&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;completion_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">156&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;total_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">201&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;system_fingerprint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fp_8bda4d3a2c&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-different-models">Test Different Models&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with GPT-4o&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Explain the difference between GPT-4o and GPT-4o-mini in one sentence.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.choices[0].message.content&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with different temperature settings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Write a creative haiku about AI gateways.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;temperature&amp;#34;: 1.2,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.choices[0].message.content&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-embeddings">Test Embeddings&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test text embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/embeddings&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;input&amp;#34;: &amp;#34;agentgateway provides unified access to multiple AI providers with enterprise-grade security and observability.&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;text-embedding-3-small&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> usage: .usage,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> embedding_length: (.data[0].embedding | length),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> first_few_values: (.data[0].embedding[0:5])
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-models-list">Test Models List&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># List available models&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/models&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[] | select(.id | contains(&amp;#34;gpt&amp;#34;)) | .id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="monitoring-real-ai-requests">Monitoring Real AI Requests&lt;/h2>
&lt;h3 id="view-metrics-in-grafana">View Metrics in Grafana&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Open Grafana&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n monitoring svc/grafana-prometheus 3000:3000 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Grafana: http://localhost:3000 (admin / &lt;/span>&lt;span class="nv">$GRAFANA_ADMIN_PASSWORD&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Navigate to agentgateway Dashboard&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Go to Dashboards &amp;gt; agentgateway Overview&lt;/li>
&lt;li>Send several test requests&lt;/li>
&lt;li>Observe real-time metrics:
&lt;ul>
&lt;li>Request rates by model&lt;/li>
&lt;li>Token usage (input/output)&lt;/li>
&lt;li>Response latencies&lt;/li>
&lt;li>Cost estimates&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Key Metrics to Watch&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Request Volume&lt;/strong>: Requests per second by model&lt;/li>
&lt;li>&lt;strong>Token Consumption&lt;/strong>: Input vs output tokens&lt;/li>
&lt;li>&lt;strong>Cost Tracking&lt;/strong>: Real-time cost accumulation&lt;/li>
&lt;li>&lt;strong>Latency Percentiles&lt;/strong>: P50, P95, P99 response times&lt;/li>
&lt;li>&lt;strong>Error Rates&lt;/strong>: Failed requests and reasons&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="analyze-distributed-traces">Analyze Distributed Traces&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Navigate to Explore&lt;/strong> in Grafana&lt;/li>
&lt;li>&lt;strong>Select Tempo&lt;/strong> as data source&lt;/li>
&lt;li>&lt;strong>Search for traces&lt;/strong>:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">service.name=&amp;#34;agentgateway&amp;#34; &amp;amp;&amp;amp; gen_ai.system=&amp;#34;openai&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>Examine trace details&lt;/strong>:
&lt;ul>
&lt;li>Total request duration&lt;/li>
&lt;li>Time spent in agentgateway vs OpenAI&lt;/li>
&lt;li>Token usage attributes&lt;/li>
&lt;li>Error information (if any)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="real-time-log-analysis">Real-Time Log Analysis&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># View structured logs with LLM context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> jq &lt;span class="s1">&amp;#39;select(.gen_ai) | {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> timestamp: .timestamp,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .gen_ai.request.model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> prompt_tokens: .gen_ai.usage.prompt_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> completion_tokens: .gen_ai.usage.completion_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> duration: .duration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Monitor costs in real-time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway -f &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> grep -E &lt;span class="s2">&amp;#34;(usage|cost)&amp;#34;&lt;/span> --line-buffered
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="performance-testing">Performance Testing&lt;/h2>
&lt;h3 id="create-load-test-for-real-openai">Create Load Test for Real OpenAI&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; load-test-openai.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_PORT=&amp;#34;${GATEWAY_PORT:-8080}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">CONCURRENT_REQUESTS=${1:-5}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">TOTAL_REQUESTS=${2:-25}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">MODEL=${3:-&amp;#34;gpt-4o-mini&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Load testing OpenAI through agentgateway...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Target: $GATEWAY_IP:$GATEWAY_PORT&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Model: $MODEL&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Concurrent: $CONCURRENT_REQUESTS&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Total: $TOTAL_REQUESTS&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;⚠️ This will incur OpenAI API costs!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">read -p &amp;#34;Continue? (y/N): &amp;#34; -n 1 -r
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">if [[ ! $REPLY =~ ^[Yy]$ ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Cancelled.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> exit 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Array of test prompts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">PROMPTS=(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;Explain what an AI Gateway is in 2 sentences.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;What are the benefits of using Kubernetes for AI workloads?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;How does distributed tracing help with AI observability?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;What is the importance of rate limiting in AI services?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;Describe the role of authentication in AI gateways.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">send_request() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local id=$1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local prompt=&amp;#34;${PROMPTS[$((id % ${#PROMPTS[@]}))]}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local start_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local response=$(curl -s -w &amp;#34;%{http_code}&amp;#34; &amp;#34;$GATEWAY_IP:$GATEWAY_PORT/openai/chat/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;x-request-id: load-test-$id&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#34;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;model\&amp;#34;: \&amp;#34;$MODEL\&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;messages\&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {\&amp;#34;role\&amp;#34;: \&amp;#34;user\&amp;#34;, \&amp;#34;content\&amp;#34;: \&amp;#34;$prompt\&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;max_tokens\&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local end_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local duration=$(echo &amp;#34;$end_time - $start_time&amp;#34; | bc -l)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local http_code=&amp;#34;${response: -3}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [ &amp;#34;$http_code&amp;#34; = &amp;#34;200&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local tokens=$(echo &amp;#34;${response%???}&amp;#34; | jq -r &amp;#39;.usage.total_tokens // 0&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;Request %3d: ✓ %s (%.2fs, %d tokens)\n&amp;#34; $id $http_code $duration $tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;Request %3d: ✗ %s (%.2fs)\n&amp;#34; $id $http_code $duration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Starting load test...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">start_time=$(date +%s)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for ((i=1; i&amp;lt;=TOTAL_REQUESTS; i++)); do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> send_request $i &amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Control concurrency
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if ((i % CONCURRENT_REQUESTS == 0)); then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> wait
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">wait
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">end_time=$(date +%s)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">total_duration=$((end_time - start_time))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Load test complete!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Total time: ${total_duration}s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Average rate: $(echo &amp;#34;scale=2; $TOTAL_REQUESTS / $total_duration&amp;#34; | bc) req/s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Check Grafana for detailed metrics and cost tracking.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x load-test-openai.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Run a small test (optional - will incur API costs)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ./load-test-openai.sh 3 10 &amp;#34;gpt-4o-mini&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cost-tracking-and-management">Cost Tracking and Management&lt;/h2>
&lt;h3 id="understanding-token-costs">Understanding Token Costs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create cost calculation script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; calculate-costs.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># OpenAI pricing (as of 2024 - verify current pricing)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_MINI_INPUT_COST=0.000150 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_MINI_OUTPUT_COST=0.000600 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_INPUT_COST=0.0025 # per 1K tokens 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_OUTPUT_COST=0.0100 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EMBEDDING_COST=0.000020 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Analyzing recent token usage...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Extract token usage from logs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail=100 | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq -r &amp;#39;select(.gen_ai.usage) | [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .timestamp,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.request.model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.usage.prompt_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.usage.completion_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.usage.total_tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ] | @csv&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> awk -F&amp;#39;,&amp;#39; &amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">BEGIN {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Timestamp,Model,Input Tokens,Output Tokens,Total Tokens,Estimated Cost&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_cost = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model = $2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> input = $3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> output = $4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Remove quotes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, model)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (model ~ /gpt-4o-mini/) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = (input * 0.000150 / 1000) + (output * 0.000600 / 1000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> } else if (model ~ /gpt-4o/) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = (input * 0.0025 / 1000) + (output * 0.0100 / 1000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> } else if (model ~ /embedding/) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = ((input + output) * 0.000020 / 1000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_cost += cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;%s,%s,%d,%d,%d,$%.6f\n&amp;#34;, $1, model, input, output, input+output, cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">END {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;\nTotal estimated cost: $%.6f\n&amp;#34;, total_cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x calculate-costs.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./calculate-costs.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="set-up-cost-alerts">Set Up Cost Alerts&lt;/h3>
&lt;p>Create a simple cost monitoring script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; cost-monitor.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">COST_THRESHOLD=${1:-1.00} # Alert if costs exceed $1.00
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">CHECK_INTERVAL=${2:-300} # Check every 5 minutes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Monitoring OpenAI costs through agentgateway...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Threshold: \$$COST_THRESHOLD&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Check interval: ${CHECK_INTERVAL}s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">while true; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Calculate recent costs (last hour)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> RECENT_COST=$(kubectl logs deploy/agentgateway -n enterprise-agentgateway --since=1h | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq -r &amp;#39;select(.gen_ai.usage) | .gen_ai.usage&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> awk &amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if ($0 ~ /gpt-4o-mini/) cost += 0.0008
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else if ($0 ~ /gpt-4o/) cost += 0.015
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else cost += 0.0002
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> } END {printf &amp;#34;%.4f&amp;#34;, cost}&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;$(date): Recent cost: \$$RECENT_COST&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (( $(echo &amp;#34;$RECENT_COST &amp;gt; $COST_THRESHOLD&amp;#34; | bc -l) )); then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;🚨 ALERT: Cost threshold exceeded! \$$RECENT_COST &amp;gt; \$$COST_THRESHOLD&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Add notification logic here (email, Slack, etc.)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sleep $CHECK_INTERVAL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x cost-monitor.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Run in background: ./cost-monitor.sh 0.50 &amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="comparing-mock-vs-real-provider">Comparing Mock vs Real Provider&lt;/h2>
&lt;h3 id="run-comparison-test">Run Comparison Test&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; compare-mock-real.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">TEST_PROMPT=&amp;#34;What are three key features of an AI Gateway?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Comparing Mock vs Real OpenAI responses...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Test prompt: $TEST_PROMPT&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Function to test an endpoint
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local endpoint=$1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local label=$2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== $label ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local start_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local response=$(curl -s &amp;#34;$GATEWAY_IP:8080/$endpoint/chat/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;authorization: bearer mock-token&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#34;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;model\&amp;#34;: \&amp;#34;gpt-4o-mini\&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;messages\&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {\&amp;#34;role\&amp;#34;: \&amp;#34;user\&amp;#34;, \&amp;#34;content\&amp;#34;: \&amp;#34;$TEST_PROMPT\&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;max_tokens\&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local end_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local duration=$(echo &amp;#34;scale=3; $end_time - $start_time&amp;#34; | bc -l)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Response time: ${duration}s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Content: $(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.choices[0].message.content&amp;#39;)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Tokens: $(echo &amp;#34;$response&amp;#34; | jq &amp;#39;.usage&amp;#39;)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Test both endpoints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;mock-openai&amp;#34; &amp;#34;Mock OpenAI&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;openai&amp;#34; &amp;#34;Real OpenAI&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Key Differences:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;- Mock: Deterministic, no cost, instant response&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;- Real: AI-generated, costs money, variable latency&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;- Both: Provide observability data for monitoring&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x compare-mock-real.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ./compare-mock-real.sh&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="troubleshooting-real-provider-issues">Troubleshooting Real Provider Issues&lt;/h2>
&lt;h3 id="common-issues-and-solutions">Common Issues and Solutions&lt;/h3>
&lt;p>&lt;strong>1. Authentication Errors (401)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret exists and is properly formatted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test API key directly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check agentgateway logs for auth errors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway &lt;span class="p">|&lt;/span> grep -i auth
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. Rate Limit Errors (429)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check for rate limit errors in logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway &lt;span class="p">|&lt;/span> grep -i &lt;span class="s2">&amp;#34;rate\|429&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Implement backoff in requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;, &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;test&amp;#34;}]}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. Model Not Found (404)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># List available models&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/openai/models&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[] | .id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Try with a known working model&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;test&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>4. Timeout Issues&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check backend timeout configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway -o yaml &lt;span class="p">|&lt;/span> grep -A &lt;span class="m">5&lt;/span> timeout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Increase timeout if needed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl patch agentgatewaybackend openai-all-models -n enterprise-agentgateway --type&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;merge&amp;#39;&lt;/span> -p&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;spec&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;policies&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;timeout&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;request&amp;#34;: &amp;#34;180s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="debug-connection-issues">Debug Connection Issues&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test connectivity from agentgateway pod&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n enterprise-agentgateway deploy/agentgateway -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -v https://api.openai.com/v1/models &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check DNS resolution&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n enterprise-agentgateway deploy/agentgateway -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> nslookup api.openai.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify backend status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="environment-management">Environment Management&lt;/h2>
&lt;p>Create a helper script for managing your OpenAI integration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; openai-management.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">case $1 in
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;test&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Testing OpenAI integration...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> curl -s &amp;#34;${GATEWAY_IP:-localhost}:8080/openai/chat/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{&amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;, &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello, test message&amp;#34;}]}&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq &amp;#39;.choices[0].message.content&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;models&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Available OpenAI models:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> curl -s &amp;#34;${GATEWAY_IP:-localhost}:8080/openai/models&amp;#34; | jq -r &amp;#39;.data[] | select(.id | test(&amp;#34;gpt|embedding&amp;#34;)) | .id&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;status&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== OpenAI Backend Status ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== OpenAI Routes ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get httproute -n enterprise-agentgateway -l provider=openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;logs&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl logs deploy/agentgateway -n enterprise-agentgateway | grep -i openai | tail -20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;costs&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Calculating recent costs...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ./calculate-costs.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;cleanup&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Removing OpenAI configuration...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete httproute -n enterprise-agentgateway -l provider=openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete secret openai-secret -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> *)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Usage: $0 {test|models|status|logs|costs|cleanup}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Commands:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; test - Send test request to OpenAI&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; models - List available models&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; status - Show backend and route status&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; logs - Show recent OpenAI-related logs&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; costs - Calculate token costs&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; cleanup - Remove all OpenAI configuration&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">esac
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x openai-management.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>With your real OpenAI integration working, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Add more providers&lt;/strong> - Configure Anthropic, AWS Bedrock, or Azure OpenAI&lt;/li>
&lt;li>&lt;strong>Implement security&lt;/strong> - Add authentication, rate limiting, and guardrails&lt;/li>
&lt;li>&lt;strong>Explore advanced routing&lt;/strong> - Path-based, header-based, and weighted routing&lt;/li>
&lt;li>&lt;strong>Set up production monitoring&lt;/strong> - Alerts, dashboards, and cost controls&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll explore advanced routing patterns that allow you to route requests to different models or providers based on various criteria.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Real AI integration&lt;/strong> requires proper API key management and security&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> provides immediate insights into costs, performance, and usage&lt;/li>
&lt;li>&lt;strong>agentgateway&lt;/strong> adds minimal latency while providing significant value&lt;/li>
&lt;li>&lt;strong>Production considerations&lt;/strong> include timeouts, retries, and cost monitoring&lt;/li>
&lt;li>&lt;strong>Structured logging&lt;/strong> and metrics enable comprehensive AI operations visibility&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway now connects to production AI services while maintaining enterprise-grade security, observability, and cost control!&lt;/p></description><content:encoded>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>Now that you have agentgateway running with observability and have tested with a mock environment, it&amp;rsquo;s time to connect to a real AI provider. OpenAI is the perfect starting point due to its widespread adoption, comprehensive API, and excellent documentation.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll create your first production-ready route to OpenAI, implement proper security practices, and demonstrate the observability benefits of routing AI traffic through agentgateway.&lt;/p>
&lt;h2 id="what-youll-learn">What You&amp;rsquo;ll Learn&lt;/h2>
&lt;ul>
&lt;li>Create secure API key storage for OpenAI&lt;/li>
&lt;li>Configure an AgentgatewayBackend for OpenAI&lt;/li>
&lt;li>Set up HTTPRoutes for different OpenAI endpoints&lt;/li>
&lt;li>Test chat completions, embeddings, and model listings&lt;/li>
&lt;li>Monitor real AI requests through Grafana&lt;/li>
&lt;li>Understand token usage and cost tracking&lt;/li>
&lt;li>Compare mock vs real provider behavior&lt;/li>
&lt;/ul>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Completed previous blog posts (agentgateway setup, observability, mock environment)&lt;/li>
&lt;li>&lt;strong>Valid OpenAI API Key&lt;/strong> with credits (get from &lt;a href="https://platform.openai.com">OpenAI Platform&lt;/a>)&lt;/li>
&lt;li>Kind cluster with agentgateway and monitoring running&lt;/li>
&lt;li>Basic understanding of OpenAI API structure&lt;/li>
&lt;/ul>
&lt;h2 id="environment-setup">Environment Setup&lt;/h2>
&lt;h3 id="prepare-your-openai-api-key">Prepare Your OpenAI API Key&lt;/h3>
&lt;p>First, ensure you have a valid OpenAI API key with appropriate permissions:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Visit OpenAI Platform&lt;/strong>: Go to &lt;a href="https://platform.openai.com">platform.openai.com&lt;/a>&lt;/li>
&lt;li>&lt;strong>Create API Key&lt;/strong>: Navigate to API Keys section and create a new key&lt;/li>
&lt;li>&lt;strong>Set Usage Limits&lt;/strong>: Configure spending limits to control costs&lt;/li>
&lt;li>&lt;strong>Note the Key&lt;/strong>: Copy your API key securely&lt;/li>
&lt;/ol>
&lt;h3 id="set-environment-variables">Set Environment Variables&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Set your OpenAI API key (replace with your actual key)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-your-openai-api-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify other environment variables&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOLO_TRIAL_LICENSE_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;your-license-key-here&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">ENTERPRISE_AGW_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;2.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify the key is set correctly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;OpenAI API Key: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">OPENAI_API_KEY&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nv">0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="nv">10&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test the key directly (optional)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0:3] | .[].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="creating-secure-api-key-storage">Creating Secure API Key Storage&lt;/h2>
&lt;h3 id="create-kubernetes-secret">Create Kubernetes Secret&lt;/h3>
&lt;p>Store your OpenAI API key securely in a Kubernetes secret:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create secret with proper authorization header format&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl create secret generic openai-secret &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --from-literal&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Authorization=Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dry-run&lt;span class="o">=&lt;/span>client -o yaml &lt;span class="p">|&lt;/span> kubectl apply -f -
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret creation&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># View secret structure (without revealing the key)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n enterprise-agentgateway -o yaml &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sed &lt;span class="s1">&amp;#39;s/Authorization:.*/Authorization: [REDACTED]/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="security-best-practices">Security Best Practices&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Label the secret for better organization&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl label secret openai-secret -n enterprise-agentgateway &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">provider&lt;/span>&lt;span class="o">=&lt;/span>openai &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">purpose&lt;/span>&lt;span class="o">=&lt;/span>api-authentication
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret permissions&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl auth can-i get secrets -n enterprise-agentgateway --as&lt;span class="o">=&lt;/span>system:serviceaccount:enterprise-agentgateway:default
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="configuring-openai-backend">Configuring OpenAI Backend&lt;/h2>
&lt;h3 id="create-agentgatewaybackend">Create AgentgatewayBackend&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: agentgateway.dev/v1alpha1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> environment: production
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure OpenAI as the AI provider
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> openai:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Use default OpenAI endpoint (api.openai.com)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Optionally specify config like:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # config:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # endpoint: &amp;#34;https://api.openai.com&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # model: &amp;#34;gpt-4o-mini&amp;#34; # Default model override
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Authentication policy using our secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> policies:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> auth:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> secretRef:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-secret
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Key defaults to &amp;#34;Authorization&amp;#34; if not specified
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Optional: Configure timeout and retry policies
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeout:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34; # 2 minutes for long-running requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Optional: Add retry policy for resilience
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retry:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> attempts: 3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backoff:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> baseInterval: &amp;#34;1s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> maxInterval: &amp;#34;10s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> retryOn:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;5xx&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;reset&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;connect-failure&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - &amp;#34;refused-stream&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-backend-configuration">Verify Backend Configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check backend status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Ensure backend is accepted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway -o &lt;span class="nv">jsonpath&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{.status.conditions[?(@.type==&amp;#34;Accepted&amp;#34;)].status}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="creating-openai-routes">Creating OpenAI Routes&lt;/h2>
&lt;h3 id="basic-chat-completions-route">Basic Chat Completions Route&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-chat
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: chat-completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Match requests to /openai/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Configure timeouts for potentially long AI requests
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;120s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Transform the path to OpenAI&amp;#39;s expected format
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/chat/completions
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="models-and-embeddings-routes">Models and Embeddings Routes&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl apply -f- &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai/models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">apiVersion: gateway.networking.k8s.io/v1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kind: HTTPRoute
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">metadata:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> name: openai-embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> labels:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> provider: openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> endpoint: embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">spec:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> parentRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> namespace: enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> rules:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - matches:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: PathPrefix
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> value: /openai/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> backendRefs:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - name: openai-all-models
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> group: agentgateway.dev
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kind: AgentgatewayBackend
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> timeouts:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> request: &amp;#34;60s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> filters:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> - type: URLRewrite
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> urlRewrite:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> path:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> type: ReplacePrefixMatch
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> replacePrefixMatch: /v1/embeddings
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="verify-routes">Verify Routes&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check all OpenAI routes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute -n enterprise-agentgateway -l &lt;span class="nv">provider&lt;/span>&lt;span class="o">=&lt;/span>openai
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify route status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get httproute openai-chat -n enterprise-agentgateway -o yaml &lt;span class="p">|&lt;/span> grep -A &lt;span class="m">10&lt;/span> status:
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="testing-your-openai-integration">Testing Your OpenAI Integration&lt;/h2>
&lt;h3 id="get-gateway-endpoint">Get Gateway Endpoint&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Get gateway service details&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc -n enterprise-agentgateway --selector&lt;span class="o">=&lt;/span>gateway.networking.k8s.io/gateway-name&lt;span class="o">=&lt;/span>agentgateway
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># For kind clusters, use localhost&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_IP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;localhost&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">GATEWAY_PORT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;8080&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;agentgateway available at: &lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-chat-completions">Test Chat Completions&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test basic chat completion&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;x-request-id: test-&lt;/span>&lt;span class="k">$(&lt;/span>date +%s&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;system&amp;#34;, 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;You are a helpful assistant that explains AI Gateway technology.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> },
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;What are the key benefits of using an AI Gateway like agentgateway for managing LLM requests?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 200,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;temperature&amp;#34;: 0.7
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Expected response:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chatcmpl-abc123def456&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;object&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;chat.completion&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1701234567&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;gpt-4o-mini-2024-07-18&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;choices&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;index&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;assistant&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;An AI Gateway like agentgateway provides several key benefits for managing LLM requests:\n\n1. **Unified Access**: Single endpoint for multiple AI providers (OpenAI, Anthropic, AWS Bedrock)\n2. **Observability**: Comprehensive metrics, logging, and tracing of AI requests\n3. **Cost Management**: Token usage tracking and cost attribution across teams\n4. **Security**: Centralized authentication, API key management, and access control\n5. **Rate Limiting**: Protect against excessive usage and manage quotas\n6. **Reliability**: Failover between providers, retry policies, and circuit breakers\n\nThis centralized approach simplifies AI operations while providing enterprise-grade governance.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;finish_reason&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;stop&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;usage&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;prompt_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">45&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;completion_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">156&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;total_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">201&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;system_fingerprint&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fp_8bda4d3a2c&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-different-models">Test Different Models&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with GPT-4o&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Explain the difference between GPT-4o and GPT-4o-mini in one sentence.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.choices[0].message.content&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test with different temperature settings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Write a creative haiku about AI gateways.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;temperature&amp;#34;: 1.2,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.choices[0].message.content&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-embeddings">Test Embeddings&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test text embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/embeddings&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;input&amp;#34;: &amp;#34;agentgateway provides unified access to multiple AI providers with enterprise-grade security and observability.&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;text-embedding-3-small&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> usage: .usage,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> embedding_length: (.data[0].embedding | length),
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> first_few_values: (.data[0].embedding[0:5])
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="test-models-list">Test Models List&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># List available models&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:&lt;/span>&lt;span class="nv">$GATEWAY_PORT&lt;/span>&lt;span class="s2">/openai/models&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[] | select(.id | contains(&amp;#34;gpt&amp;#34;)) | .id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="monitoring-real-ai-requests">Monitoring Real AI Requests&lt;/h2>
&lt;h3 id="view-metrics-in-grafana">View Metrics in Grafana&lt;/h3>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Open Grafana&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl port-forward -n monitoring svc/grafana-prometheus 3000:3000 &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Grafana: http://localhost:3000 (admin / &lt;/span>&lt;span class="nv">$GRAFANA_ADMIN_PASSWORD&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>Navigate to agentgateway Dashboard&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Go to Dashboards &amp;gt; agentgateway Overview&lt;/li>
&lt;li>Send several test requests&lt;/li>
&lt;li>Observe real-time metrics:
&lt;ul>
&lt;li>Request rates by model&lt;/li>
&lt;li>Token usage (input/output)&lt;/li>
&lt;li>Response latencies&lt;/li>
&lt;li>Cost estimates&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Key Metrics to Watch&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Request Volume&lt;/strong>: Requests per second by model&lt;/li>
&lt;li>&lt;strong>Token Consumption&lt;/strong>: Input vs output tokens&lt;/li>
&lt;li>&lt;strong>Cost Tracking&lt;/strong>: Real-time cost accumulation&lt;/li>
&lt;li>&lt;strong>Latency Percentiles&lt;/strong>: P50, P95, P99 response times&lt;/li>
&lt;li>&lt;strong>Error Rates&lt;/strong>: Failed requests and reasons&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="analyze-distributed-traces">Analyze Distributed Traces&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Navigate to Explore&lt;/strong> in Grafana&lt;/li>
&lt;li>&lt;strong>Select Tempo&lt;/strong> as data source&lt;/li>
&lt;li>&lt;strong>Search for traces&lt;/strong>:
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">service.name=&amp;#34;agentgateway&amp;#34; &amp;amp;&amp;amp; gen_ai.system=&amp;#34;openai&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>&lt;strong>Examine trace details&lt;/strong>:
&lt;ul>
&lt;li>Total request duration&lt;/li>
&lt;li>Time spent in agentgateway vs OpenAI&lt;/li>
&lt;li>Token usage attributes&lt;/li>
&lt;li>Error information (if any)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ol>
&lt;h3 id="real-time-log-analysis">Real-Time Log Analysis&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># View structured logs with LLM context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> jq &lt;span class="s1">&amp;#39;select(.gen_ai) | {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> timestamp: .timestamp,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> model: .gen_ai.request.model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> prompt_tokens: .gen_ai.usage.prompt_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> completion_tokens: .gen_ai.usage.completion_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> duration: .duration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Monitor costs in real-time&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway -f &lt;span class="p">|&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> grep -E &lt;span class="s2">&amp;#34;(usage|cost)&amp;#34;&lt;/span> --line-buffered
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="performance-testing">Performance Testing&lt;/h2>
&lt;h3 id="create-load-test-for-real-openai">Create Load Test for Real OpenAI&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; load-test-openai.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_PORT=&amp;#34;${GATEWAY_PORT:-8080}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">CONCURRENT_REQUESTS=${1:-5}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">TOTAL_REQUESTS=${2:-25}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">MODEL=${3:-&amp;#34;gpt-4o-mini&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Load testing OpenAI through agentgateway...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Target: $GATEWAY_IP:$GATEWAY_PORT&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Model: $MODEL&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Concurrent: $CONCURRENT_REQUESTS&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Total: $TOTAL_REQUESTS&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;⚠️ This will incur OpenAI API costs!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">read -p &amp;#34;Continue? (y/N): &amp;#34; -n 1 -r
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">if [[ ! $REPLY =~ ^[Yy]$ ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Cancelled.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> exit 1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Array of test prompts
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">PROMPTS=(
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;Explain what an AI Gateway is in 2 sentences.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;What are the benefits of using Kubernetes for AI workloads?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;How does distributed tracing help with AI observability?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;What is the importance of rate limiting in AI services?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;Describe the role of authentication in AI gateways.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">send_request() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local id=$1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local prompt=&amp;#34;${PROMPTS[$((id % ${#PROMPTS[@]}))]}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local start_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local response=$(curl -s -w &amp;#34;%{http_code}&amp;#34; &amp;#34;$GATEWAY_IP:$GATEWAY_PORT/openai/chat/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;x-request-id: load-test-$id&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#34;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;model\&amp;#34;: \&amp;#34;$MODEL\&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;messages\&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {\&amp;#34;role\&amp;#34;: \&amp;#34;user\&amp;#34;, \&amp;#34;content\&amp;#34;: \&amp;#34;$prompt\&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;max_tokens\&amp;#34;: 50
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local end_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local duration=$(echo &amp;#34;$end_time - $start_time&amp;#34; | bc -l)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local http_code=&amp;#34;${response: -3}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if [ &amp;#34;$http_code&amp;#34; = &amp;#34;200&amp;#34; ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local tokens=$(echo &amp;#34;${response%???}&amp;#34; | jq -r &amp;#39;.usage.total_tokens // 0&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;Request %3d: ✓ %s (%.2fs, %d tokens)\n&amp;#34; $id $http_code $duration $tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;Request %3d: ✗ %s (%.2fs)\n&amp;#34; $id $http_code $duration
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Starting load test...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">start_time=$(date +%s)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">for ((i=1; i&amp;lt;=TOTAL_REQUESTS; i++)); do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> send_request $i &amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Control concurrency
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if ((i % CONCURRENT_REQUESTS == 0)); then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> wait
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">wait
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">end_time=$(date +%s)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">total_duration=$((end_time - start_time))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Load test complete!&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Total time: ${total_duration}s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Average rate: $(echo &amp;#34;scale=2; $TOTAL_REQUESTS / $total_duration&amp;#34; | bc) req/s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Check Grafana for detailed metrics and cost tracking.&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x load-test-openai.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Run a small test (optional - will incur API costs)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ./load-test-openai.sh 3 10 &amp;#34;gpt-4o-mini&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cost-tracking-and-management">Cost Tracking and Management&lt;/h2>
&lt;h3 id="understanding-token-costs">Understanding Token Costs&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create cost calculation script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; calculate-costs.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># OpenAI pricing (as of 2024 - verify current pricing)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_MINI_INPUT_COST=0.000150 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_MINI_OUTPUT_COST=0.000600 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_INPUT_COST=0.0025 # per 1K tokens 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GPT4O_OUTPUT_COST=0.0100 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EMBEDDING_COST=0.000020 # per 1K tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Analyzing recent token usage...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Extract token usage from logs
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">kubectl logs deploy/agentgateway -n enterprise-agentgateway --tail=100 | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq -r &amp;#39;select(.gen_ai.usage) | [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .timestamp,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.request.model,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.usage.prompt_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.usage.completion_tokens,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> .gen_ai.usage.total_tokens
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ] | @csv&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> awk -F&amp;#39;,&amp;#39; &amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">BEGIN {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> print &amp;#34;Timestamp,Model,Input Tokens,Output Tokens,Total Tokens,Estimated Cost&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_cost = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> model = $2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> input = $3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> output = $4
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Remove quotes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> gsub(/&amp;#34;/, &amp;#34;&amp;#34;, model)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = 0
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (model ~ /gpt-4o-mini/) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = (input * 0.000150 / 1000) + (output * 0.000600 / 1000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> } else if (model ~ /gpt-4o/) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = (input * 0.0025 / 1000) + (output * 0.0100 / 1000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> } else if (model ~ /embedding/) {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> cost = ((input + output) * 0.000020 / 1000)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> total_cost += cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;%s,%s,%d,%d,%d,$%.6f\n&amp;#34;, $1, model, input, output, input+output, cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">END {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> printf &amp;#34;\nTotal estimated cost: $%.6f\n&amp;#34;, total_cost
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x calculate-costs.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">./calculate-costs.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="set-up-cost-alerts">Set Up Cost Alerts&lt;/h3>
&lt;p>Create a simple cost monitoring script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; cost-monitor.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">COST_THRESHOLD=${1:-1.00} # Alert if costs exceed $1.00
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">CHECK_INTERVAL=${2:-300} # Check every 5 minutes
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Monitoring OpenAI costs through agentgateway...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Threshold: \$$COST_THRESHOLD&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Check interval: ${CHECK_INTERVAL}s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">while true; do
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Calculate recent costs (last hour)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> RECENT_COST=$(kubectl logs deploy/agentgateway -n enterprise-agentgateway --since=1h | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq -r &amp;#39;select(.gen_ai.usage) | .gen_ai.usage&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> awk &amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if ($0 ~ /gpt-4o-mini/) cost += 0.0008
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else if ($0 ~ /gpt-4o/) cost += 0.015
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> else cost += 0.0002
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> } END {printf &amp;#34;%.4f&amp;#34;, cost}&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;$(date): Recent cost: \$$RECENT_COST&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> if (( $(echo &amp;#34;$RECENT_COST &amp;gt; $COST_THRESHOLD&amp;#34; | bc -l) )); then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;🚨 ALERT: Cost threshold exceeded! \$$RECENT_COST &amp;gt; \$$COST_THRESHOLD&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> # Add notification logic here (email, Slack, etc.)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> fi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> sleep $CHECK_INTERVAL
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">done
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x cost-monitor.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Run in background: ./cost-monitor.sh 0.50 &amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="comparing-mock-vs-real-provider">Comparing Mock vs Real Provider&lt;/h2>
&lt;h3 id="run-comparison-test">Run Comparison Test&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; compare-mock-real.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">GATEWAY_IP=&amp;#34;${GATEWAY_IP:-localhost}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">TEST_PROMPT=&amp;#34;What are three key features of an AI Gateway?&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Comparing Mock vs Real OpenAI responses...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Test prompt: $TEST_PROMPT&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Function to test an endpoint
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint() {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local endpoint=$1
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local label=$2
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== $label ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local start_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local response=$(curl -s &amp;#34;$GATEWAY_IP:8080/$endpoint/chat/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;authorization: bearer mock-token&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#34;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;model\&amp;#34;: \&amp;#34;gpt-4o-mini\&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;messages\&amp;#34;: [
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> {\&amp;#34;role\&amp;#34;: \&amp;#34;user\&amp;#34;, \&amp;#34;content\&amp;#34;: \&amp;#34;$TEST_PROMPT\&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> \&amp;#34;max_tokens\&amp;#34;: 100
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> }&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local end_time=$(date +%s.%N)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> local duration=$(echo &amp;#34;scale=3; $end_time - $start_time&amp;#34; | bc -l)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> 
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Response time: ${duration}s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Content: $(echo &amp;#34;$response&amp;#34; | jq -r &amp;#39;.choices[0].message.content&amp;#39;)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Tokens: $(echo &amp;#34;$response&amp;#34; | jq &amp;#39;.usage&amp;#39;)&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"># Test both endpoints
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;mock-openai&amp;#34; &amp;#34;Mock OpenAI&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">test_endpoint &amp;#34;openai&amp;#34; &amp;#34;Real OpenAI&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;Key Differences:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;- Mock: Deterministic, no cost, instant response&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;- Real: AI-generated, costs money, variable latency&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">echo &amp;#34;- Both: Provide observability data for monitoring&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x compare-mock-real.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ./compare-mock-real.sh&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="troubleshooting-real-provider-issues">Troubleshooting Real Provider Issues&lt;/h2>
&lt;h3 id="common-issues-and-solutions">Common Issues and Solutions&lt;/h3>
&lt;p>&lt;strong>1. Authentication Errors (401)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify secret exists and is properly formatted&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get secret openai-secret -n enterprise-agentgateway -o yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test API key directly&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;https://api.openai.com/v1/models&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0].id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check agentgateway logs for auth errors&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway &lt;span class="p">|&lt;/span> grep -i auth
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2. Rate Limit Errors (429)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check for rate limit errors in logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl logs deploy/agentgateway -n enterprise-agentgateway &lt;span class="p">|&lt;/span> grep -i &lt;span class="s2">&amp;#34;rate\|429&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Implement backoff in requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -i &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{&amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;, &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;test&amp;#34;}]}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>3. Model Not Found (404)&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># List available models&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/openai/models&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[] | .id&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Try with a known working model&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$GATEWAY_IP&lt;/span>&lt;span class="s2">:8080/openai/chat/completions&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;content-type: application/json&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;test&amp;#34;}]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>4. Timeout Issues&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check backend timeout configuration&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway -o yaml &lt;span class="p">|&lt;/span> grep -A &lt;span class="m">5&lt;/span> timeout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Increase timeout if needed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl patch agentgatewaybackend openai-all-models -n enterprise-agentgateway --type&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;merge&amp;#39;&lt;/span> -p&lt;span class="o">=&lt;/span>&lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;spec&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;policies&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;timeout&amp;#34;: {
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;request&amp;#34;: &amp;#34;180s&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1">}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="debug-connection-issues">Debug Connection Issues&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Test connectivity from agentgateway pod&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n enterprise-agentgateway deploy/agentgateway -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -v https://api.openai.com/v1/models &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="nv">$OPENAI_API_KEY&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Check DNS resolution&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> -n enterprise-agentgateway deploy/agentgateway -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> nslookup api.openai.com
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verify backend status&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="environment-management">Environment Management&lt;/h2>
&lt;p>Create a helper script for managing your OpenAI integration:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cat &lt;span class="s">&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39; &amp;gt; openai-management.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">case $1 in
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;test&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Testing OpenAI integration...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> curl -s &amp;#34;${GATEWAY_IP:-localhost}:8080/openai/chat/completions&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -H &amp;#34;content-type: application/json&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d &amp;#39;{&amp;#34;model&amp;#34;: &amp;#34;gpt-4o-mini&amp;#34;, &amp;#34;messages&amp;#34;: [{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: &amp;#34;Hello, test message&amp;#34;}]}&amp;#39; | \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> jq &amp;#39;.choices[0].message.content&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;models&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Available OpenAI models:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> curl -s &amp;#34;${GATEWAY_IP:-localhost}:8080/openai/models&amp;#34; | jq -r &amp;#39;.data[] | select(.id | test(&amp;#34;gpt|embedding&amp;#34;)) | .id&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;status&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== OpenAI Backend Status ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;=== OpenAI Routes ===&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl get httproute -n enterprise-agentgateway -l provider=openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;logs&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl logs deploy/agentgateway -n enterprise-agentgateway | grep -i openai | tail -20
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;costs&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Calculating recent costs...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ./calculate-costs.sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> &amp;#34;cleanup&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Removing OpenAI configuration...&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete httproute -n enterprise-agentgateway -l provider=openai
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete agentgatewaybackend openai-all-models -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> kubectl delete secret openai-secret -n enterprise-agentgateway
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> *)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Usage: $0 {test|models|status|logs|costs|cleanup}&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34;Commands:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; test - Send test request to OpenAI&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; models - List available models&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; status - Show backend and route status&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; logs - Show recent OpenAI-related logs&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; costs - Calculate token costs&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> echo &amp;#34; cleanup - Remove all OpenAI configuration&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> ;;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">esac
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">EOF&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">chmod +x openai-management.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="next-steps">Next Steps&lt;/h2>
&lt;p>With your real OpenAI integration working, you&amp;rsquo;re ready to:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Add more providers&lt;/strong> - Configure Anthropic, AWS Bedrock, or Azure OpenAI&lt;/li>
&lt;li>&lt;strong>Implement security&lt;/strong> - Add authentication, rate limiting, and guardrails&lt;/li>
&lt;li>&lt;strong>Explore advanced routing&lt;/strong> - Path-based, header-based, and weighted routing&lt;/li>
&lt;li>&lt;strong>Set up production monitoring&lt;/strong> - Alerts, dashboards, and cost controls&lt;/li>
&lt;/ol>
&lt;p>In our next blog post, we&amp;rsquo;ll explore advanced routing patterns that allow you to route requests to different models or providers based on various criteria.&lt;/p>
&lt;h2 id="key-takeaways">Key Takeaways&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Real AI integration&lt;/strong> requires proper API key management and security&lt;/li>
&lt;li>&lt;strong>Observability&lt;/strong> provides immediate insights into costs, performance, and usage&lt;/li>
&lt;li>&lt;strong>agentgateway&lt;/strong> adds minimal latency while providing significant value&lt;/li>
&lt;li>&lt;strong>Production considerations&lt;/strong> include timeouts, retries, and cost monitoring&lt;/li>
&lt;li>&lt;strong>Structured logging&lt;/strong> and metrics enable comprehensive AI operations visibility&lt;/li>
&lt;/ul>
&lt;p>Your agentgateway now connects to production AI services while maintaining enterprise-grade security, observability, and cost control!&lt;/p></content:encoded></item><item><title>Cloud Architecture Fundamentals</title><link>https://maniak.io/articles/cloud-architecture-fundamentals/</link><pubDate>Wed, 04 Feb 2026 00:00:00 +0000</pubDate><guid>https://maniak.io/articles/cloud-architecture-fundamentals/</guid><description>&lt;p>Cloud isn&amp;rsquo;t just someone else&amp;rsquo;s computer. It&amp;rsquo;s a different way of thinking about infrastructure entirely.&lt;/p>
&lt;h2 id="core-principles">Core Principles&lt;/h2>
&lt;p>&lt;strong>Design for failure.&lt;/strong> Every component will eventually fail. The question is whether your system survives it.&lt;/p>
&lt;p>&lt;strong>Automate everything.&lt;/strong> If a human has to do it more than once, it should be automated. Manual processes are the enemy of reliability.&lt;/p>
&lt;p>&lt;strong>Observe relentlessly.&lt;/strong> You can&amp;rsquo;t fix what you can&amp;rsquo;t see. Monitoring, logging, and tracing aren&amp;rsquo;t optional.&lt;/p>
&lt;h2 id="the-stack-matters-less-than-you-think">The Stack Matters Less Than You Think&lt;/h2>
&lt;p>Whether you&amp;rsquo;re running on AWS, Azure, or GCP, the principles remain the same. The cloud provider is a tool. Your architecture is the strategy.&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;The best architecture is the one your team can operate at 3 AM without thinking.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Focus on simplicity. Focus on resilience. The rest follows.&lt;/p></description><content:encoded>&lt;p>Cloud isn&amp;rsquo;t just someone else&amp;rsquo;s computer. It&amp;rsquo;s a different way of thinking about infrastructure entirely.&lt;/p>
&lt;h2 id="core-principles">Core Principles&lt;/h2>
&lt;p>&lt;strong>Design for failure.&lt;/strong> Every component will eventually fail. The question is whether your system survives it.&lt;/p>
&lt;p>&lt;strong>Automate everything.&lt;/strong> If a human has to do it more than once, it should be automated. Manual processes are the enemy of reliability.&lt;/p>
&lt;p>&lt;strong>Observe relentlessly.&lt;/strong> You can&amp;rsquo;t fix what you can&amp;rsquo;t see. Monitoring, logging, and tracing aren&amp;rsquo;t optional.&lt;/p>
&lt;h2 id="the-stack-matters-less-than-you-think">The Stack Matters Less Than You Think&lt;/h2>
&lt;p>Whether you&amp;rsquo;re running on AWS, Azure, or GCP, the principles remain the same. The cloud provider is a tool. Your architecture is the strategy.&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;The best architecture is the one your team can operate at 3 AM without thinking.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Focus on simplicity. Focus on resilience. The rest follows.&lt;/p></content:encoded></item></channel></rss>