From a15fbfbd44fbfcdf63fa943996394f8ca05bf109 Mon Sep 17 00:00:00 2001 From: Rachael Graham Date: Tue, 22 Sep 2026 14:28:36 -0500 Subject: [PATCH] Recent page reviews Self-reviews and edits of pages I recently added i quickly to make sure they were in the 1.0.0-alpha1 release, but that I hadnt had the chance to test and review more closely. Signed-off-by: Rachael Graham --- .../content/kagent/1.x/about/core-concepts.md | 13 ++-- .../kagent/1.x/examples/agentgateway.md | 66 ++++++++++++++----- .../kagent/1.x/observability/tracing.md | 2 +- .../networking-and-egress.md | 49 +++++++------- 4 files changed, 81 insertions(+), 49 deletions(-) diff --git a/docs-site/content/kagent/1.x/about/core-concepts.md b/docs-site/content/kagent/1.x/about/core-concepts.md index 2947d27d..6fce57c4 100644 --- a/docs-site/content/kagent/1.x/about/core-concepts.md +++ b/docs-site/content/kagent/1.x/about/core-concepts.md @@ -69,7 +69,7 @@ An **AgentTemplate** is a Kubernetes custom resource that defines _what an agent - **Model configuration**: The large language model (LLM) provider and model the agent uses. This is the only field an AgentTemplate strictly requires. - **System prompt**: A literal prompt, or a Go-templated one that can `include` shared ConfigMaps. -- **Tools**: A list of {{< gloss "Tool binding" >}}tool bindings{{< /gloss >}} that the agent can call. Each binding is either a {{< gloss "Model Context Protocol" >}}Model Context Protocol{{< /gloss >}} (MCP) server, or another AgentTemplate used as an agent tool (see [Agent tools](#agent-tools-shared-vs-dedicated)). +- **Tools**: A list of {{< gloss "Tool binding" >}}tool bindings{{< /gloss >}} that the agent can call. Each binding is either a {{< gloss "Model Context Protocol" >}}Model Context Protocol{{< /gloss >}} (MCP) server, or another AgentTemplate used as an agent tool (see [Agent tools](#agent-tools)). - **Skills** and **plugins**: Reusable capability packages, sourced from an Open Container Initiative (OCI) registry, Git, or S3. An AgentTemplate does nothing on its own. It becomes runnable once it is paired with a Harness whose `allowedAgentTemplates` selector accepts it. @@ -101,13 +101,10 @@ An **Actor** is the sandboxed unit of compute, provided by [Agent Substrate]({{< Actors are the reason why AgentInstances can suspend and resume cheaply instead of staying resident. An idle Actor can be snapshotted and torn down, then resumed from that snapshot on demand. To understand the full mechanics ({{< gloss "Worker" >}}Workers{{< /gloss >}}, {{< gloss "WorkerPool" >}}WorkerPools{{< /gloss >}}, ActorTemplates, and snapshotting), see [Agent Substrate architecture]({{< link path="about/agent-substrate" >}}). -## Agent tools: Shared vs. Dedicated +## Agent tools -An AgentTemplate's tools are not limited to MCP servers. A tool binding can also point at another AgentTemplate, letting one agent call another agent as a tool. Each agent-tool binding picks an isolation mode: +An AgentTemplate's tools are not limited to MCP servers. A tool binding can also point at another AgentTemplate, which lets one agent hand work to a specialist agent. -- **Shared** (default): The child agent runs inside the same Actor as its parent. This option is cheaper, but the child shares its parent's fate: if the parent's Actor is suspended or crashes, so does the child. -- **Dedicated**: The child agent gets its own Actor, isolated from its parent. This option is more expensive, but a crash or a long-running task in the child cannot take down the parent, and the child can be scaled, suspended, or resumed independently. +Every agent-tool binding carries an isolation mode, and `Shared` is the only mode that kagent implements today. A `Shared` binding runs the bound agent inside its parent's Actor, so the nesting costs no extra compute and the two agents share one sandbox. The schema also accepts `Dedicated`, which would give the bound agent an Actor of its own, but a binding that sets it fails to compile and the pair never becomes ready. For both modes and the state of `Dedicated`, see [Shared and Dedicated isolation]({{< link path="skills-and-mcp/about-tools#shared-and-dedicated-isolation" >}}). -Shared nesting never goes more than one level deep. A Shared agent tool can have Dedicated agent tools beneath it, but it cannot contain another Shared one. - -This limit keeps the model predictable. A Dedicated binding gives the child its own Actor. A Shared binding puts the child in its parent's Actor, and because Shared bindings cannot chain, that parent always has an Actor of its own. Working out where any agent runs is therefore never more than a single step. +Because a `Shared` binding nests one agent inside another's runtime boundary, the compiler constrains the shape of the resulting tree. Nesting stops at one level, so a bound agent cannot itself bind a third. That cap keeps the model predictable: every agent runs either in its own Actor or in the Actor of the agent that bound it, so working out where any agent runs is never more than a single step. For the rest of the rules that a tree must satisfy, see [What a Shared tree allows]({{< link path="skills-and-mcp/about-tools#what-a-shared-tree-allows" >}}). diff --git a/docs-site/content/kagent/1.x/examples/agentgateway.md b/docs-site/content/kagent/1.x/examples/agentgateway.md index d0f2d7f9..30d2f0d1 100644 --- a/docs-site/content/kagent/1.x/examples/agentgateway.md +++ b/docs-site/content/kagent/1.x/examples/agentgateway.md @@ -5,9 +5,9 @@ weight: 80 author: kagent.dev --- -An agent sends every turn of a conversation to a model provider, and each of those requests carries whatever the person typed. When agentgateway routes that traffic, the gateway sees each request before the provider does, so you can inspect and stop a prompt at the gateway. This example adds a prompt guard to the model that an agent calls, then watches the gateway reject a prompt that carries an email address. +An agent sends every turn of a conversation to a model provider, and each of those requests carries whatever the person typed, personally identifiable information (PII) included. When agentgateway routes that traffic, the gateway sees each request before the provider does, so you can inspect and stop a prompt at the gateway. This example adds a prompt guard to the model that an agent calls, then watches the gateway reject a prompt that carries an email address. -[Agentgateway model routing]({{< link path="setup/model-providers/byo-agentgateway#set-up-agentgateway-model-routing" >}}) sets up the routing that this example governs. Read that page first, because the steps here extend the `AgentgatewayModel` and the {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} that it creates. +This guide builds on the routing configured in [Agentgateway model routing]({{< link path="setup/model-providers/byo-agentgateway#set-up-agentgateway-model-routing" >}}), including extending the `AgentgatewayModel` and the {{< gloss "ModelConfig" >}}ModelConfig{{< /gloss >}} that those steps create. ## About prompt guards on a model @@ -15,10 +15,10 @@ A prompt guard inspects the body of an OpenAI-compatible request as the request A guard can live in two places, and the choice follows the routing that you already use: -- **On the `AgentgatewayModel`**, under `spec.policies.promptGuard`. The guard applies to that one model, and no other resource is involved. This example uses this form, because the routing that `byo-agentgateway.md` documents attaches models straight to a `Gateway` listener. -- **On an `AgentgatewayPolicy`** that targets an `HTTPRoute`. An `AgentgatewayPolicy` cannot name an `AgentgatewayModel` in its `targetRefs`, so this form requires the model to hang off an HTTPRoute rather than off the listener. For that variant, see [Attach the guard to a route instead](#attach-the-guard-to-a-route-instead). +- **On the `AgentgatewayModel`**, under `spec.policies.promptGuard`. The guard applies to that one model, and no other resource is involved. This example uses this form, because [agentgateway model routing]({{< link path="setup/model-providers/byo-agentgateway#set-up-agentgateway-model-routing" >}}) attaches models straight to a `Gateway` listener. +- **On an `AgentgatewayPolicy`** that targets an `HTTPRoute`. An `AgentgatewayPolicy` cannot name an `AgentgatewayModel` in its `targetRefs`, so this form requires the model to attach to an HTTPRoute rather than to the listener. For more information about this variant, see [Attach the guard to a route instead](#attach-the-guard-to-a-route-instead). -The following `AgentgatewayModel` carries the guard that the rest of this example applies. `provider` and `parentRefs` route the model, and `policies` adds the guard. +The following `AgentgatewayModel` carries the guard that the rest of this example applies. ```yaml apiVersion: agentgateway.dev/v1alpha1 @@ -34,6 +34,9 @@ spec: sectionName: http provider: OpenAI policies: + auth: + secretRef: + name: openai-key promptGuard: request: - regex: @@ -46,13 +49,15 @@ spec: message: The prompt contained personally identifiable information. ``` -A guard needs only `regex`. The remaining fields take defaults, and those defaults are permissive, so a guard that omits `action` masks the match rather than rejecting the request. +Only `policies.promptGuard` is new in this example. The routing fields and `policies.auth` come from the prerequisite setup, and they appear here because a reapply must carry them. | Field | Description | | ----- | ----------- | -| `policies.promptGuard.request[]` | The guards to apply to requests that the agent sends. A separate `response` list guards what the provider sends back. | -| `regex.builtins` | Built-in patterns for common personally identifiable information (PII). The five values are `Email`, `Ssn`, `CreditCard`, `PhoneNumber`, and `CaSin`. To add your own patterns, use `regex.matches`, which holds a list of regular expressions and is additive with `builtins`. | -| `regex.action` | What to do with a match, either `Reject` or `Mask`. Omit to default to `Mask`, which is also the safer choice for an agent. A `Reject` guard ends the conversation permanently, for the reason described in [Mask instead of reject](#mask-instead-of-reject). | +| `parentRefs` and `provider` | Route the model to a `Gateway` listener, and name the provider that agentgateway calls. | +| `policies.auth` | The Secret holding the credentials that agentgateway uses to reach the provider. | +| `policies.promptGuard.request[]` | The guards to apply to requests that the agent sends. Each entry sets exactly one guard kind: `regex` matches patterns in the gateway itself, while `webhook`, `openAIModeration`, `bedrockGuardrails`, and `googleModelArmor` hand the content to an external service instead. A separate `response` list guards what the provider sends back. | +| `regex.builtins` | Built-in patterns for common kinds of PII. The five values are `Email`, `Ssn`, `CreditCard`, `PhoneNumber`, and `CaSin`. To add your own patterns, use `regex.matches`, which holds a list of regular expressions and is additive with `builtins`. | +| `regex.action` | What to do with a match, one of `Mask`, `Reject`, and `Audit`. Omit to default to `Mask`, which is also the safer choice for an agent. `Audit` records the action that the guard would have taken and lets the content through, so use it to trial a guard before you enforce it. A `Reject` guard ends the conversation permanently, for the reason described in [Mask instead of reject](#mask-instead-of-reject). | | `response.message` | The message that the gateway returns to the caller on a rejection. Omit to default to `The request was rejected due to inappropriate content`. A sibling `response.statusCode` field sets the status code, and defaults to `403`. | > [!IMPORTANT] @@ -68,7 +73,7 @@ A guard needs only `regex`. The remaining fields take defaults, and those defaul ## Add the prompt guard to the model -Adding `policies` to a model that already routes traffic changes nothing about the routing, so you reapply the same resource with the guard attached. +Adding `promptGuard` to a model that already routes traffic changes nothing about the routing, so you reapply the same resource with the guard attached. Keep `policies.auth` in the resource that you reapply, because an apply replaces `policies` wholesale. A model that loses its `auth` is still accepted and programmed, and every request through it returns the provider's own `401`. 1. Reapply the `AgentgatewayModel` with a request guard that rejects three kinds of PII. ```bash @@ -86,6 +91,9 @@ Adding `policies` to a model that already routes traffic changes nothing about t sectionName: http provider: OpenAI policies: + auth: + secretRef: + name: openai-key promptGuard: request: - regex: @@ -112,7 +120,7 @@ Adding `policies` to a model that already routes traffic changes nothing about t ## Watch the gateway reject a prompt -Two checks are worth running in order. Calling the gateway directly isolates the guard from anything that kagent does, and sending the same content through an agent then shows what a person talking to the agent experiences. +Two paths are worth checking in order. Calling the gateway directly isolates the guard from anything that kagent does, and sending the same content through an agent then shows what a person talking to the agent experiences. 1. Reach the gateway. Choose the tab that matches your cluster. @@ -145,7 +153,7 @@ Two checks are worth running in order. Calling the gateway directly isolates the The prompt contained personally identifiable information. ``` -3. Send a prompt that carries no PII, to confirm that ordinary traffic still reaches the provider. +3. To confirm that ordinary traffic still reaches the provider, send a prompt that carries no PII. ```bash curl -i http://$AGENTGATEWAY_ADDRESS/v1/chat/completions \ -H "Content-Type: application/json" \ @@ -209,7 +217,7 @@ Two checks are worth running in order. Calling the gateway directly isolates the ## Mask instead of reject -Masking is the better default for an agent, for the reason the preceding section demonstrates: a `Reject` guard ends the conversation for good, while a `Mask` guard lets the turn through with the matched text replaced. Reserve `Reject` for a policy that forbids PII outright and accepts a dead conversation as the price. +A `Reject` guard ends the conversation for good, as the [previous section](#watch-the-gateway-reject-a-prompt) demonstrates. A `Mask` guard instead lets the turn through with the matched text replaced. Masking is therefore the better default for an agent, and `Reject` belongs to a policy that forbids PII outright and accepts a dead conversation as the price. Masking also repairs a conversation that a `Reject` guard already stopped. Changing the action re-masks the offending message on the next turn rather than rejecting it, so the stranded AgentInstance answers again without being recreated. @@ -225,14 +233,14 @@ Masking also repairs a conversation that a `Reject` guard already stopped. Chang --task "Summarize this ticket: alex@example.com reports that checkout returns 503 errors during peak hours." ``` - The turn succeeds this time. The agent answers, or asks a follow-up question, and the address never reaches the provider. The patch replaces the whole `request` list, so it also drops the `response.message` that the rejection used; a `Mask` guard returns no message, because it rejects nothing. + The turn succeeds this time. The agent answers, or asks a follow-up question, and the address never reaches the provider. The patch replaces the whole `request` list, so it also drops the `response.message` that the rejection used. A `Mask` guard returns no message, because it rejects nothing. > [!WARNING] > **A guard on the response does not inspect streamed content unless you enable it.** Prompt guards default to skipping streaming responses to preserve throughput. Set `policies.promptGuard.streaming: Enabled` to guard them, and note that `Mask` is never applied to a streamed response even then: a guard can reject streamed content, and matched text in a stream that is not rejected passes through unmodified. ## Guard what an agent's tools send -An agent differs from a chat client in that a good deal of its traffic originates from tools rather than from a person. A tool that reads a ticket, a database row, or a Kubernetes resource can feed PII back to the model, and the default scope does not inspect that content. +An agent differs from a chat client in that much of its traffic originates from tools rather than from a person. A tool that reads a ticket, a database row, or a Kubernetes resource can feed PII back to the model, and the default scope does not inspect that content. Add `scope` to the guard to cover tool traffic. `ToolOutput` covers the results that a tool feeds back to the model, and `ToolInput` covers the arguments that the model produces for a tool call. @@ -289,9 +297,15 @@ An `AgentgatewayPolicy` holds the same `promptGuard` configuration, and one poli kind: HTTPRoute name: model-traffic provider: OpenAI + policies: + auth: + secretRef: + name: openai-key EOF ``` + The model keeps `policies.auth` and drops `policies.promptGuard`, because the `AgentgatewayPolicy` that you create next carries the guard. The `parentRef` needs no `sectionName`, because the route has exactly one rule. + 2. Create the `AgentgatewayPolicy` that targets the route rule. ```bash kubectl apply -f - < The controller passes its tracing configuration to the `kagent`, `codex`, and `claude` runtimes. Each of the three exports on its own instrumentation, so the span names in this page describe the `kagent` runtime and do not carry over to the other two. An agent on the `byo` runtime receives no tracing configuration, and its half of the trace is missing. For the available runtimes, see [Choose a runtime]({{< link path="agents/agent-harness#choose-a-runtime" >}}). > [!NOTE] -> A `byo` image that implements OTel itself reads the exporter variables from the Harness `spec.env`, which the controller leaves alone for this runtime. Its spans still do not reach a collector inside the cluster, because kagent adds the collector to an Actor's egress allowlist only for the runtimes it configures, and no field adds a host to that list by hand. For more information, see [Networking and egress control]({{< link path="substrate-runtime/networking-and-egress#what-kagent-puts-in-the-policy" >}}). +> A `byo` image that implements OTel itself reads the exporter variables from the Harness `spec.env`, which the controller leaves alone for this runtime. Its spans still do not reach a collector inside the cluster, because kagent adds the collector to an Actor's egress allowlist only for the runtimes it configures, and no field adds a host to that list by hand. For more information, see [Networking and egress control]({{< link path="substrate-runtime/networking-and-egress#policy-generation" >}}). Both processes report themselves as separate OpenTelemetry (OTel) services. A tracing backend uses these service names to group the spans. diff --git a/docs-site/content/kagent/1.x/substrate-runtime/networking-and-egress.md b/docs-site/content/kagent/1.x/substrate-runtime/networking-and-egress.md index 0790aea4..467f38a0 100644 --- a/docs-site/content/kagent/1.x/substrate-runtime/networking-and-egress.md +++ b/docs-site/content/kagent/1.x/substrate-runtime/networking-and-egress.md @@ -5,36 +5,37 @@ weight: 40 author: kagent.dev --- -An agent reaches the network only through Agent Substrate's egress gateway, and the gateway authorizes every connection against a policy attached to that {{< gloss "Actor" >}}Actor{{< /gloss >}}. The policy is default-deny: an Actor with no policy makes no outbound request at all. kagent builds the policy for you from the AgentTemplate, which is why a documented agent works without any egress configuration of its own. +An agent reaches the network only through Agent Substrate's egress gateway, and the gateway authorizes every connection against a policy attached to that {{< gloss "Actor" >}}Actor{{< /gloss >}}. The policy is default-deny: an Actor with no policy makes no outbound request at all. kagent builds the policy for you from the AgentTemplate. A documented agent therefore works without any egress configuration of its own. > [!IMPORTANT] -> How much a hostname rule constrains depends on what the gateway can read. A request the gateway reads in the clear is matched against the policy's hostnames. An encrypted connection is not: the gateway sees only the address the Actor dialed, so a policy made only of hostname rules does not restrict which HTTPS destination an Actor reaches. Treat the allowlist as a control over cleartext traffic, and as a record of intent for the rest, until you deploy the TLS-terminating gateway. For the detail, see [What the gateway can read](#what-the-gateway-can-read). +> The gateway terminates TLS, so hostname rules constrain encrypted traffic as well as cleartext. An HTTPS request is decrypted at the gateway, matched against the policy's hostnames, and re-originated to the destination. An opaque TCP tunnel is the exception, because it carries no request for the gateway to read. For details, see [Traffic types](#traffic-types). For the inbound half of the picture, and for the NetworkPolicy that protects Workers, see [Sandboxing]({{< link path="substrate-runtime/sandboxing#default-network-posture" >}}). -## How the gateway decides +## How the gateway authorizes a connection -Outbound traffic leaves a Worker through the egress gateway rather than going straight out, which gives Agent Substrate one place to identify the caller and apply rules. The gateway does both on every connection. +Outbound traffic leaves a Worker through the egress gateway rather than directly out. That single chokepoint gives Agent Substrate one place to identify the caller and apply rules, and the gateway does both on every connection. -It identifies the Actor from the client certificate that the connection presents, reading the Actor's SPIFFE ID out of a verified certificate rather than from anything in the request. Nothing running inside the tunnel can set or change that identity, and a request that arrives without one is refused. Two Actors that dial the same address never share a connection pool, so neither inherits the other's identity. +The gateway identifies the Actor from the client certificate that the connection presents, reading the Actor's SPIFFE ID out of a verified certificate rather than from anything in the request. Nothing running inside the tunnel can set or change that identity, and a request that arrives without one is refused. Two Actors that dial the same address never share a connection pool, so neither inherits the other's identity. -It then authorizes the request against the Actor's `EgressPolicy`. An Actor has at most one, named `default`. The policy holds an ordered list of rules, and the gateway takes the **first** rule that matches: only that rule applies, and evaluation stops even when a later rule would also match. **A request that matches no rule is denied, and an Actor with no policy at all gets no outbound connection.** Which rules can match a given request depends on what the gateway can read of it, as the next section describes. +The gateway then authorizes the request against the Actor's `EgressPolicy`. An Actor has at most one, named `default`. The policy holds an ordered list of rules, and the gateway takes the **first** rule that matches: only that rule applies, and evaluation stops even when a later rule would also match. **A request that matches no rule is denied, and an Actor with no policy at all gets no outbound connection.** A rule can match only what the gateway can read of the request. > [!NOTE] > Rule order is significant, and a policy holds at most 256 rules. Because the first match wins, a broad rule placed early makes every narrower rule after it unreachable. -### What the gateway can read +### Traffic types -The gateway decides a request on its `Host` and on the address the Actor dialed, and only the first of those survives encryption. That splits egress into two cases worth keeping apart. +The gateway allows or denies a request on one of two things: the `Host` header of the request, or the address the Actor dialed. Which of the two it uses depends on whether the traffic carries a request that the gateway can read. That difference splits egress into three cases worth keeping apart. -- **Cleartext HTTP.** The gateway reads the `Host` header, so hostname rules apply and a request that matches no rule is refused. This is the case where the allowlist does the work you would expect. -- **TLS.** The default gateway does not terminate TLS, so it cannot read the request inside the tunnel. The connection is decided at its outer hop, by address. A policy that holds hostname rules but no address rule lets that connection open, because a request inside it might have been allowed by name, and nothing then checks the name. **The practical result is that hostname rules do not restrict HTTPS destinations on a default installation.** +- **Cleartext HTTP**: The gateway reads the `Host` header, so hostname rules apply and a request that matches no rule is refused. +- **TLS**: The gateway terminates the connection with a certificate minted for the destination name, reads the request inside, and matches it against hostname rules exactly as it does cleartext. It then re-originates its own TLS connection to the destination. An allowed request appears in the gateway log with the `http.host` and `http.path` it was matched on. +- **Opaque TCP**: A tunnel that carries no HTTP request gives the gateway nothing to read, so the gateway allows or denies it by address, at its outer hop. Express those destinations as a `cidrs` rule, because a hostname rule has nothing to match against. -Agent Substrate ships a second, TLS-terminating gateway that re-originates the connection and evaluates the inner request the same way as cleartext. Deploy it if you need hostname rules enforced against HTTPS. To restrict encrypted traffic without it, express the destination as a `cidrs` rule, which the outer hop can evaluate. +Enforcing hostname rules against encrypted traffic has a cost: because the gateway terminates TLS, it holds the cleartext of every agent request for as long as it takes to authorize it. Holding the cleartext also lets the gateway attach credentials on the agent's behalf. For more information, see [Credential injection](#credential-injection). -## What a rule can match +## Rule matching -Each rule matches on exactly one kind of destination. Which kind it is also decides how the gateway dials the connection, so the two are worth reading together. +Each rule matches on exactly one kind of destination, and that kind also determines how the gateway dials a connection that the rule allows. | Rule | Matches | How the gateway dials a match | | ---- | ------- | ----------------------------- | @@ -46,23 +47,23 @@ A hostname pattern is a DNS name, optionally with a `*` wildcard replacing the c A CIDR prefix must be canonical, with every bit after the prefix length set to zero, such as `192.0.2.0/24` or `2001:db8::/32`. -## What kagent puts in the policy +## Policy generation kagent derives the allowlist from the AgentTemplate rather than taking it as configuration, so the policy always describes what the agent was actually compiled to need. Each {{< gloss "Revision" >}}revision{{< /gloss >}} records the hosts its agent depends on: - The model provider endpoint, from the ModelConfig. -- Every RemoteMCPServer, HTTP tool, and SSE tool the AgentTemplate binds. +- Every RemoteMCPServer, HTTP tool, and SSE tool that the AgentTemplate binds. - Every skill artifact source, such as a git or OCI registry host. - The OpenTelemetry collector, for each telemetry signal that is enabled. -kagent compiles that set into the Actor's policy as it creates the Actor, as one `hostnames` rule and, for any destination given as a literal IP address, one `cidrs` rule. +kagent compiles that set into the Actor's policy as it creates the Actor. Each destination that carries a credential takes its own `hostnames` rule, placed first so that the rule holding the credential is the one that matches. The remaining destinations follow as a single `hostnames` rule, and any destination given as a literal IP address becomes one `cidrs` rule at the end. > [!IMPORTANT] -> No field adds an arbitrary host to the allowlist. A destination becomes reachable by being named in the AgentTemplate as a model endpoint, an MCP server, an HTTP tool, or a skill source. Because kagent writes these as hostname rules, they are enforced against cleartext requests rather than against encrypted ones, so read this allowlist as the set of destinations the agent is *meant* to reach rather than as the limit of what it *can* reach over HTTPS. +> No field adds an arbitrary host to the allowlist. A destination becomes reachable by being named in the AgentTemplate as a model endpoint, an MCP server, an HTTP tool, or a skill source. kagent writes these as hostname rules, which the gateway enforces against encrypted and cleartext requests alike, so the allowlist is the limit of what an agent can reach over HTTP and HTTPS rather than only a record of intent. -Agents on the `kagent`, `codex`, and `claude` runtimes get their collector host added automatically. An agent on the `byo` runtime does not, so a BYO image that exports its own telemetry has no route to a collector. For more information, see [Tracing]({{< link path="observability/tracing" >}}). +The collector host for agents on the `kagent`, `codex`, and `claude` runtimes is added automatically. The host for an agent on the `byo` runtime is not, so a BYO image that exports its own telemetry has no route to a collector. For more information, see [Tracing]({{< link path="observability/tracing" >}}). -## When a policy changes +## Policy changes The gateway caches each Actor's policy rather than fetching it per request, so a change takes effect on a delay rather than instantly. The cache holds an entry for 10 seconds by default, and that interval is the upper bound on the lag: a policy that is created, updated, or deleted reaches new requests within one interval. Deleting a policy becomes a denial rather than an absence of one. @@ -84,13 +85,15 @@ error request ... http.host=kagent-tools.kagent http.status=403 error="actor egress policy denied: ... \"EgressPolicy not found\"" reason=Authorization ``` -Read `reason=Authorization` as the policy refusing the request, and the `error` text as which check failed. An allowed request is logged the same way, with its status and, for an encrypted connection, the `tls.sni` it was opened for, which makes the log the quickest way to see what an agent actually reaches. +Read `reason=Authorization` as the policy refusing the request, and the `error` text as which check failed. An allowed request is logged the same way, with its status, the `http.host` and `http.path` it was matched on, and the `substrate.connect.authority` address the Actor originally dialed. The log is therefore the quickest way to see what an agent actually reaches. The compiled policy is not readable through `kubectl` or `kagent`, so treat the AgentTemplate as the source of truth for what an agent is meant to reach. If a destination is missing, it is missing from the AgentTemplate. ## Credential injection -An egress rule can declare that the gateway attach a credential to a matching request, so that an agent reaches a protected service without ever holding the secret itself. A `hostnames` rule carries the effect, which names the header to set, an optional prefix such as `Bearer `, and a `substrate-secret://` reference that a registered credential provider resolves. +An egress rule can declare that the gateway attach a credential to a matching request, so that an agent reaches a protected service without ever holding the secret itself. A `hostnames` rule carries the effect, which names the header to set, an optional prefix such as `Bearer `, and an `ate-secret://` reference that a registered credential provider resolves. -> [!WARNING] -> Credential injection is declared in the API but is not yet implemented. A request that matches a rule declaring one is **denied with a 501 response** rather than forwarded without the credential, so do not add the effect to a policy expecting it to be ignored. kagent does not generate rules that use it. +kagent writes these effects for you. Every ModelConfig that holds an `apiKeySecret` becomes a credential binding on the rule for that model's host, as does every RemoteMCPServer whose `headersFrom` reads a Secret. The gateway resolves the reference as the request passes through, so the agent's own environment never holds the key. + +> [!NOTE] +> The gateway injects a credential only on a connection that it terminates, and only when a credential provider is configured for the reference's authority. Where either is missing, the request reaches the destination **without** the credential rather than being denied. Once injection is attempted and fails, the request fails closed: `403` when the provider does not hold the credential or refuses to release it, and `503` when the provider is unreachable.