diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 788ffd5f..0a3cadb3 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -50,7 +50,6 @@ on: - 'docs-site/layouts/**' - 'docs-site/static/**' - 'docs-site/hugo.yaml' - - 'docs-site/hugo.preview.yaml' - 'docs-site/go.mod' - 'docs-site/go.sum' - '.github/workflows/links.yml' @@ -111,17 +110,7 @@ jobs: # docs-site's CSS pipeline shells out to tooling in its own # devDependencies, so install them first so Hugo finds the binaries. npm ci - # The preview overlay and -D are what put content/kagent/1.x/ in this - # build at all. That section is withheld from production by a draft - # cascade in 1.x/_index.md, so a plain `hugo --config hugo.yaml` - # renders ZERO 1.x pages and this job passes green having scanned - # none of them. The overlay also restores the 1.x entry in - # params.versions, without which every {{< version include-if="1.x" >}} - # renders empty. Both are temporary: at the 1.0 release the draft keys - # and hugo.preview.yaml go away, and this line reverts to - # `hugo --config hugo.yaml --gc --minify`. The Makefile and - # preview.yaml layer the overlay the same way. - hugo --config hugo.yaml,hugo.preview.yaml -D --gc --minify + hugo --config hugo.yaml --gc --minify - name: Prepare workspace run: mkdir -p artifacts diff --git a/.github/workflows/playwright-screenshots.yaml b/.github/workflows/playwright-screenshots.yaml index 7d3236fe..d1c9e437 100644 --- a/.github/workflows/playwright-screenshots.yaml +++ b/.github/workflows/playwright-screenshots.yaml @@ -10,9 +10,12 @@ on: options: [mock, cluster, both] push: branches: - # The 1.x docs live on this branch until the 1.0 release; retarget to main - # when 1.x is published and the draft cascade is dropped. - - kagent-1-0-docs + # Retargeted from the kagent-1-0-docs feature branch on 2026-09-16, once that + # branch merged and main took ownership of the harness, the baselines, and the + # guides that embed them. Keep this on main: a feature branch gets deleted when + # it merges, and a push trigger that names a deleted branch simply stops firing, + # which is indistinguishable from a clean run. + - main paths: # Only the inputs that actually determine the pixels. Everything else in the # repo can change without a capture being stale. @@ -130,7 +133,7 @@ jobs: Generated by the [**Refresh kagent UI screenshots** workflow](https://github.com/${{ github.repository_owner }}/website/actions/workflows/playwright-screenshots.yaml). branch: playwright/screenshot-refresh-mock delete-branch: true - base: kagent-1-0-docs + base: main labels: | documentation automated pr @@ -249,7 +252,7 @@ jobs: Generated by the [**Refresh kagent UI screenshots** workflow](https://github.com/${{ github.repository_owner }}/website/actions/workflows/playwright-screenshots.yaml). branch: playwright/screenshot-refresh-cluster delete-branch: true - base: kagent-1-0-docs + base: main labels: | documentation automated pr diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml index 28133701..1776d1d0 100644 --- a/.github/workflows/preview.yaml +++ b/.github/workflows/preview.yaml @@ -50,7 +50,7 @@ jobs: # OpenNext Worker (which bundles public/ as static assets). HUGO=hugo uses # the Hugo installed above instead of the local hugo160 alias. - name: Build (Hugo docs + inject into /docs + Worker) - run: make build HUGO=hugo HUGO_CONFIG=hugo.yaml,hugo.preview.yaml HUGO_FLAGS=-D + run: make build HUGO=hugo # Derive a stable preview alias from the PR branch name. Cloudflare preview # aliases must be a valid subdomain label (lowercase alphanumerics and diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index 96b31919..d3b6a9cb 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -63,6 +63,9 @@ concurrency: # --display-name kagent --out-dir docs-site/content/kagent/0.x/resources/cli \ # --url-prefix /docs/kagent/0.x/resources/cli --weight 1 # +# No --link-prefix there on purpose: 0.x is frozen and is not rebased into the +# enterprise hub, so its generated cross-links stay root-relative URLs. +# # The section is named reference/ in 1.x, not resources/ as in 0.x, and the # provider pages link to crd-ref-docs anchors (#modelconfigspec, #openaiconfig, # ...), so keep that renderer and its anchor naming. @@ -94,6 +97,11 @@ jobs: KAGENT_CLI_DIR: docs-site/content/kagent/1.x/reference/cli KMCP_CLI_DIR: docs-site/content/kmcp/reference/cli KAGENT_CLI_URL_PREFIX: /docs/kagent/1.x/reference/cli + # Version-root-relative path behind the `link` shortcode in the + # generated kagent pages. kagent is versioned and its pages are + # rebased into the enterprise docs hub, so its cross-links must not + # hardcode the 1.x segment. kmcp is unversioned and passes none. + KAGENT_CLI_LINK_PREFIX: reference/cli KMCP_CLI_URL_PREFIX: /docs/kmcp/reference/cli steps: @@ -140,11 +148,17 @@ jobs: # is NOT a source: it is gitignored and generated from Chart-template.yaml # by envsubst, so a checkout never carries a useful value. # - # Deliberately not automated: kagent.md names the UPCOMING release while - # `git describe` on main yields a dev tag, so it stays editorial until 1.0 - # ships; and jaeger / loki / tempo / otel-collector are pinned nowhere in - # kagent, so the only source is "latest upstream", which would assert - # chart versions no guide was tested against. + # Deliberately not automated: kagent.md names the release the docs + # DESCRIBE, while `git describe` on main yields a dev tag. That stayed + # editorial through 1.0 and should stay editorial now, for a second reason + # it did not have before -- the runtime image step below resolves its + # digests at the tag this conref names, so this one file is what decides + # which release the whole 1.x doc set pins to. Bumping it by hand is the + # release switch; everything else follows on the next run. + # + # Also not automated: jaeger / loki / tempo / otel-collector are pinned + # nowhere in kagent, so the only source is "latest upstream", which would + # assert chart versions no guide was tested against. # --------------------------------------------------------------------- - name: Read versions from kagent run: | @@ -182,10 +196,18 @@ jobs: echo " $1 = $2" } + # x.y.z with an OPTIONAL prerelease suffix. The suffix is not + # hypothetical: kagent 1.0 pins Agent Substrate at 0.2.0-beta4, and a + # bare-triple pattern rejected it and killed this step -- and with it + # the API reference regeneration downstream, which runs later in the + # same job. Component versions go through prerelease lines routinely; + # only the value's SHAPE is being guarded here, not its maturity. + SEMVER='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' + echo "Extracted from kagent ${KAGENT_COMMIT}:" - check SUBSTRATE_VERSION "$SUBSTRATE_VERSION" '^[0-9]+\.[0-9]+\.[0-9]+$' - check KMCP_VERSION "$KMCP_VERSION" '^[0-9]+\.[0-9]+\.[0-9]+$' - check KAGENT_TOOLS_VERSION "$KAGENT_TOOLS_VERSION" '^[0-9]+\.[0-9]+\.[0-9]+$' + check SUBSTRATE_VERSION "$SUBSTRATE_VERSION" "$SEMVER" + check KMCP_VERSION "$KMCP_VERSION" "$SEMVER" + check KAGENT_TOOLS_VERSION "$KAGENT_TOOLS_VERSION" "$SEMVER" check MAX_KUBE "$MAX_KUBE" '^[0-9]+\.[0-9]+$' { @@ -195,6 +217,110 @@ jobs: echo "MAX_KUBE=$MAX_KUBE" } >> $GITHUB_ENV + # --------------------------------------------------------------------- + # Runtime image conrefs. + # + # versions/runtime-image.md and versions/runtime-image-claude.md hold a + # FULL digest-pinned reference (repository@sha256:...), not a version, + # because a Harness `workload.image` rejects a bare tag -- a revision has + # to be reproducible. Pages reuse the whole string, so the repository name + # is not hardcoded on any page either. + # + # A digest exists only once the image is pushed, so unlike every value + # above this one is read from the REGISTRY rather than from a file in the + # kagent tree. The tag to resolve comes from versions/kagent.md, not from + # `git describe` on main: that conref names the release the docs describe, + # so resolving against it is what keeps the image a reader pins consistent + # with the chart version the install page tells them to install. It also + # means these two follow automatically whenever kagent.md is bumped by + # hand, which is the reason that one stays editorial. + # + # Image names are NOT the ones the kagent tree suggests. The kagent + # runtime is golang-adk (values.yaml calls it `agentImage`); the claude + # runtime is claude-harness. The `claude-adk` and `codex-adk` strings in + # kagent are build stages, and nothing is published under those names. + # + # 0.x needs no equivalent: it predates Harness and pins no runtime image. + # --------------------------------------------------------------------- + - name: Resolve runtime image digests + run: | + set -euo pipefail + + KAGENT_CONREF="$VERSIONS_DIR/kagent.md" + if [ ! -f "$KAGENT_CONREF" ]; then + echo "Error: expected conref $KAGENT_CONREF" + exit 1 + fi + + # The 1.x span of: + # {{< version include-if="0.x" >}}0.10.1{{< /version >}}{{< version include-if="1.x" >}}1.0.0-alpha1{{< /version >}} + IMAGE_TAG=$(sed -E 's|.*include-if="1\.x" >\}\}([^{]*)\{\{< /version >\}\}.*|\1|' "$KAGENT_CONREF") + + if ! printf '%s' "$IMAGE_TAG" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "Error: read the image tag as '$IMAGE_TAG' from $KAGENT_CONREF, which is not a version." + echo "The conref's shape likely changed. Fix the extraction here rather than editing the conref by hand." + exit 1 + fi + echo "Resolving runtime images at tag $IMAGE_TAG (from versions/kagent.md)" + + # Anonymous pull token; these packages are public. Deliberately not + # crane or `docker buildx imagetools`: neither is on this runner, and + # a HEAD against the registry needs no setup step. + ghcr_digest() { + local repo="$1" tag="$2" token + token=$(curl -fsS "https://ghcr.io/token?scope=repository:${repo}:pull&service=ghcr.io" \ + | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') || return 1 + [ -n "$token" ] || return 1 + # Index media types FIRST. These images are multi-arch and the value + # to pin is the index digest; an Accept header that omits the index + # types still succeeds but hands back ONE platform's digest, which + # would pin every reader to the runner's architecture. + curl -fsSI -H "Authorization: Bearer ${token}" \ + -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" \ + "https://ghcr.io/v2/${repo}/manifests/${tag}" \ + | awk -F': ' 'tolower($1)=="docker-content-digest"{gsub(/\r/,"");print $2}' + } + + # conref basename : GHCR repository + RUNTIME_IMAGES="runtime-image:kagent-dev/kagent/golang-adk + runtime-image-claude:kagent-dev/kagent/claude-harness" + + UNRESOLVED="" + while IFS= read -r line; do + line=$(printf '%s' "$line" | tr -d '[:space:]') + [ -n "$line" ] || continue + name="${line%%:*}" + repo="${line#*:}" + + digest=$(ghcr_digest "$repo" "$IMAGE_TAG" || true) + + # An unresolved digest is NOT fatal. versions/kagent.md is editorial + # and may legitimately name a release whose images are not pushed + # yet. Writing an empty conref would blank a `workload.image` line + # on four pages, so the conref is left alone and the miss is + # surfaced in the PR body instead -- same shape as the kmcp + # cross-check above. + if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then + echo "::warning::Could not resolve a digest for ${repo}:${IMAGE_TAG}. Leaving its conref unchanged." + UNRESOLVED="${UNRESOLVED}\`${repo}:${IMAGE_TAG}\` " + continue + fi + + # runtime-image -> RUNTIME_IMAGE, runtime-image-claude -> RUNTIME_IMAGE_CLAUDE + var=$(printf '%s' "$name" | tr 'a-z-' 'A-Z_') + # ghcr.io/ is prepended here rather than carried in the table + # above, because the table's value doubles as the registry API path, + # which takes the repository WITHOUT the host. + echo " ${name}.md = ghcr.io/${repo}@${digest}" + echo "${var}=ghcr.io/${repo}@${digest}" >> $GITHUB_ENV + done <<< "$RUNTIME_IMAGES" + + if [ -n "$UNRESOLVED" ]; then + echo "RUNTIME_IMAGE_NOTE=> [!WARNING] No digest resolved for ${UNRESOLVED}- those conrefs keep their previous value. Expected when versions/kagent.md names a release whose images are not published yet." >> $GITHUB_ENV + else + echo "RUNTIME_IMAGE_NOTE=" >> $GITHUB_ENV + fi + - name: Cross-check kmcp against its own latest release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -262,6 +388,23 @@ jobs: write_conref "$VERSIONS_DIR/max-kube.md" "$MAX_KUBE" write_conref "$KUBE_VERSION_FILE" "$MAX_KUBE" + # Runtime image conrefs, written only when the digest resolved. An + # empty value would blank the `workload.image` line on four 1.x pages, + # so an unresolved lookup keeps the previous value rather than writing + # a broken one. The miss was already warned about upstream and is + # repeated in the PR body. + echo "Runtime image conrefs:" + if [ -n "${RUNTIME_IMAGE:-}" ]; then + write_conref "$VERSIONS_DIR/runtime-image.md" "$RUNTIME_IMAGE" + else + echo " runtime-image.md: SKIPPED, unresolved digest. Left at $(cat "$VERSIONS_DIR/runtime-image.md")" + fi + if [ -n "${RUNTIME_IMAGE_CLAUDE:-}" ]; then + write_conref "$VERSIONS_DIR/runtime-image-claude.md" "$RUNTIME_IMAGE_CLAUDE" + else + echo " runtime-image-claude.md: SKIPPED, unresolved digest. Left at $(cat "$VERSIONS_DIR/runtime-image-claude.md")" + fi + # agent-substrate.md is version-split and 0.x is FROZEN: # {{< version include-if="0.x" >}}0.0.6{{< /version >}}{{< version include-if="1.x" >}}0.0.26{{< /version >}} # Replace only the 1.x span. The 0.x value documents what kagent 0.9.x @@ -681,6 +824,7 @@ jobs: --display-name kagent \ --out-dir "$KAGENT_CLI_DIR" \ --url-prefix "$KAGENT_CLI_URL_PREFIX" \ + --link-prefix "$KAGENT_CLI_LINK_PREFIX" \ --weight 30 - name: Generate kmcp CLI reference docs @@ -713,14 +857,18 @@ jobs: ${{ env.CONREF_NOTE }} - Version conrefs in this run, each read from the same place kagent's own build reads it: + ${{ env.RUNTIME_IMAGE_NOTE }} + + Version conrefs in this run. Each is read from the same place kagent's own build reads it, except the two runtime images, which are resolved from the registry because a digest exists only once the image is pushed: - | Conref | Value | Source in kagent | - | ------ | ----- | ---------------- | + | Conref | Value | Source | + | ------ | ----- | ------ | | `agent-substrate` (1.x span only) | `${{ env.SUBSTRATE_VERSION }}` | `go/go.mod` replace target | | `kmcp` | `${{ env.KMCP_VERSION }}` | `go/go.mod` require | | `kagent-tools` | `${{ env.KAGENT_TOOLS_VERSION }}` | `helm/kagent/Chart-template.yaml` | | `max-kube` (both committed copies) | `${{ env.MAX_KUBE }}` | `Makefile` `KIND_IMAGE_VERSION` | + | `runtime-image` | `${{ env.RUNTIME_IMAGE }}` | GHCR, at the tag in `versions/kagent.md` | + | `runtime-image-claude` | `${{ env.RUNTIME_IMAGE_CLAUDE }}` | GHCR, at the tag in `versions/kagent.md` | Unchanged conref values are normal: most runs touch only the generated reference pages. diff --git a/Makefile b/Makefile index d878e90b..ca43e17a 100644 --- a/Makefile +++ b/Makefile @@ -37,12 +37,9 @@ install: ## Install web + docs dependencies (npm) and Hugo modules # ── Docs (Hugo) ──────────────────────────────────────────────────────────── .PHONY: build-docs -# HUGO_CONFIG and HUGO_FLAGS let a preview build layer hugo.preview.yaml and pass -# -D without changing the production defaults. See docs-site/hugo.preview.yaml. +# HUGO_CONFIG and HUGO_FLAGS let a caller layer an extra config file or pass +# extra flags without changing the production defaults. HUGO_CONFIG ?= hugo.yaml -# serve-docs layers the preview overlay by default: without it, local authoring -# renders every {{< version include-if="1.x" >}} conref empty. See docs-site/hugo.preview.yaml. -HUGO_CONFIG_PREVIEW ?= hugo.yaml,hugo.preview.yaml HUGO_FLAGS ?= build-docs: ## Build the Hugo docs site -> docs-site/public @@ -63,7 +60,7 @@ inject-docs: ## Copy built docs into public/docs (preserves tracked assets, e.g. # back but the stylesheets never do, and the preview degrades edit by edit # instead of failing outright. Rendering to memory removes the shared directory. serve-docs: ## Preview the docs alone at http://localhost:1313/docs/ - cd $(DOCS_DIR) && $(HUGO) server --config $(HUGO_CONFIG_PREVIEW) -D --disableFastRender --renderToMemory + cd $(DOCS_DIR) && $(HUGO) server --config $(HUGO_CONFIG) --disableFastRender --renderToMemory # ── Web (Next.js) ────────────────────────────────────────────────────────── .PHONY: serve-web diff --git a/docs-site/assets/kagent-docs/snippets/name-ui.md b/docs-site/assets/kagent-docs/snippets/name-ui.md new file mode 100644 index 00000000..2f46498c --- /dev/null +++ b/docs-site/assets/kagent-docs/snippets/name-ui.md @@ -0,0 +1 @@ +kagent-ui \ No newline at end of file diff --git a/docs-site/assets/kagent-docs/versions/agent-substrate.md b/docs-site/assets/kagent-docs/versions/agent-substrate.md index 9f60ea92..d3a3eb56 100644 --- a/docs-site/assets/kagent-docs/versions/agent-substrate.md +++ b/docs-site/assets/kagent-docs/versions/agent-substrate.md @@ -1 +1 @@ -{{< version include-if="0.x" >}}0.0.9{{< /version >}}{{< version include-if="1.x" >}}0.0.26{{< /version >}} \ No newline at end of file +{{< version include-if="0.x" >}}0.0.9{{< /version >}}{{< version include-if="1.x" >}}0.2.0-beta4{{< /version >}} \ No newline at end of file diff --git a/docs-site/assets/kagent-docs/versions/kagent.md b/docs-site/assets/kagent-docs/versions/kagent.md index 3eb5ac91..705d98cb 100644 --- a/docs-site/assets/kagent-docs/versions/kagent.md +++ b/docs-site/assets/kagent-docs/versions/kagent.md @@ -1 +1 @@ -{{< version include-if="0.x" >}}0.10.1{{< /version >}}{{< version include-if="1.x" >}}1.0.0-beta0{{< /version >}} \ No newline at end of file +{{< version include-if="0.x" >}}0.10.1{{< /version >}}{{< version include-if="1.x" >}}1.0.0-alpha1{{< /version >}} \ No newline at end of file diff --git a/docs-site/assets/kagent-docs/versions/runtime-image-claude.md b/docs-site/assets/kagent-docs/versions/runtime-image-claude.md new file mode 100644 index 00000000..4593664e --- /dev/null +++ b/docs-site/assets/kagent-docs/versions/runtime-image-claude.md @@ -0,0 +1 @@ +ghcr.io/kagent-dev/kagent/claude-harness@sha256:23b59459d66ce3162892239b035ba924cd1d64a6e3826db277599e9a98b2f36a \ No newline at end of file diff --git a/docs-site/assets/kagent-docs/versions/runtime-image.md b/docs-site/assets/kagent-docs/versions/runtime-image.md new file mode 100644 index 00000000..c04e1f15 --- /dev/null +++ b/docs-site/assets/kagent-docs/versions/runtime-image.md @@ -0,0 +1 @@ +ghcr.io/kagent-dev/kagent/golang-adk@sha256:c8ab012e9774d50e20ffa8cd035ddebff69486a2843b3af281f5f6ebc67ab512 \ No newline at end of file diff --git a/docs-site/content/kagent/0.x/concepts/agents.md b/docs-site/content/kagent/0.x/concepts/agents.md index 9033942a..66452331 100644 --- a/docs-site/content/kagent/0.x/concepts/agents.md +++ b/docs-site/content/kagent/0.x/concepts/agents.md @@ -387,28 +387,6 @@ spec: | `provider.organization` | Name of the organization responsible for the agent. | | `provider.url` | URL to the agent provider's website or documentation. Must be a valid URI. | -## A2A AgentCard metadata - -When another agent or client discovers your agent over the [A2A protocol](https://a2a-protocol.org/latest/specification/#8-agent-discovery-the-agent-card), it reads a machine-readable AgentCard from your agent's `/.well-known/agent.json` endpoint. You can enrich that card with optional metadata fields on the `Agent` spec. - -```yaml -spec: - iconUrl: https://example.com/icons/my-agent.png - documentationUrl: https://docs.example.com/my-agent/ - version: "1.0.0" - provider: - organization: My Organization - url: https://example.com -``` - -| Field | Description | -|-------|-------------| -| `iconUrl` | URL to an icon image representing the agent. Must be a valid URI. | -| `documentationUrl` | URL to human-readable documentation for the agent. Must be a valid URI. | -| `version` | Version string for the agent, such as `"1.0.0"`. | -| `provider.organization` | Name of the organization responsible for the agent. | -| `provider.url` | URL to the agent provider's website or documentation. Must be a valid URI. | - ## Agents as Tools kagent also supports using agents as tools. Any agent you create can be referenced and used by other agents you have. An example use case would be to have a PromQL agent that knows how to create PromQL queries from natural language. Then you'd create a second agent that would use the PromQL agent whenever it needs to create a PromQL query. diff --git a/docs-site/content/kagent/0.x/examples/agentgateway.md b/docs-site/content/kagent/0.x/examples/agentgateway.md index 801d69f6..06f57d96 100644 --- a/docs-site/content/kagent/0.x/examples/agentgateway.md +++ b/docs-site/content/kagent/0.x/examples/agentgateway.md @@ -11,8 +11,8 @@ As your kagent deployment grows, you might need governance over how your agents ## Prerequisites 1. A running kagent installation. If you haven't installed kagent yet, follow the [quick start]({{< link path="getting-started/quickstart" >}}) guide first. -2. Follow the [agentgateway installation guide](https://agentgateway.dev/docs/kubernetes/latest/quickstart/install/) to install agentgateway in your cluster. -3. Set up an LLM provider with agentgateway. This guide uses [the Ollama setup](https://agentgateway.dev/docs/kubernetes/latest/llm/providers/ollama/) as an example. +2. Follow the [agentgateway installation guide](https://agentgateway.dev/docs/kubernetes/latest/documentation/quickstart/install/) to install agentgateway in your cluster. +3. Set up an LLM provider with agentgateway. This guide uses [the Ollama setup](https://agentgateway.dev/docs/kubernetes/latest/integrations/llm/providers/ollama/) as an example. ## Architecture @@ -97,7 +97,7 @@ With agentgateway in place, you can now apply policies to govern how your kagent ### Block requests with PII -1. Create an `AgentgatewayPolicy` resource to reject any request that contains PII, such as an email address. For more policy examples, see the [agentgateway guardrails docs](https://agentgateway.dev/docs/kubernetes/latest/llm/guardrails/regex/#block-requests-with-pii). +1. Create an `AgentgatewayPolicy` resource to reject any request that contains PII, such as an email address. For more policy examples, see the [agentgateway guardrails docs](https://agentgateway.dev/docs/kubernetes/latest/documentation/llm/guardrails/regex/#block-requests-with-pii). ```yaml kubectl apply -f - < [!NOTE] > The `AgentgatewayModel` feature is experimental and disabled by default. You must enable it when you install agentgateway by passing `--set agentgatewayModels.enabled=true` to the control plane Helm chart. -1. Install agentgateway in your cluster. For more information, see the [agentgateway documentation](https://agentgateway.dev/docs/kubernetes/latest/setup/). Add `--set agentgatewayModels.enabled=true` to the Helm command for the agentgateway control plane. +1. Install agentgateway in your cluster. For more information, see the [agentgateway documentation](https://agentgateway.dev/docs/kubernetes/latest/documentation/setup/). Add `--set agentgatewayModels.enabled=true` to the Helm command for the agentgateway control plane. 2. Create a `Gateway` resource for model routing. ```yaml @@ -39,7 +39,13 @@ You can route model requests through an [agentgateway](https://agentgateway.dev/ EOF ``` -3. Create an `AgentgatewayModel` resource for each model that you want kagent to access. The resource name becomes the model name that kagent sends in requests, so it must match `spec.model` in the kagent `ModelConfig`. The following example routes requests for `gpt-4o-mini` to the OpenAI provider. For more provider options and authentication configuration, see the [agentgateway model documentation](https://docs.solo.io/agentgateway/latest/llm/models/). +3. Store the provider credentials that agentgateway uses to call the model. Agentgateway authenticates to the provider on your agents' behalf, so this key belongs to the gateway rather than to kagent. The value is read from the Secret's `Authorization` key by default. + ```bash + kubectl create secret generic openai-key -n agentgateway-system \ + --from-file=Authorization= + ``` + +4. Create an `AgentgatewayModel` resource for each model that you want kagent to access. The resource name becomes the model name that kagent sends in requests, so it must match `spec.model` in the kagent `ModelConfig`. The following example routes requests for `gpt-4o-mini` to the OpenAI provider. For more provider options and authentication configuration, see the [agentgateway model documentation](https://docs.solo.io/agentgateway/kubernetes/latest/documentation/llm/models/). ```yaml kubectl apply -f - < [!NOTE] + > `policies.auth` gives the gateway the credentials that it needs to reach the provider. A model with no `auth` is still accepted and programmed, and every request through it returns the provider's own `401`, because agentgateway forwards the call with no credentials. + +5. Save the agentgateway Gateway service address in an environment variable. The `/v1` suffix is required: kagent appends `/chat/completions` to this value, and agentgateway serves that endpoint at `/v1/chat/completions`. Without the suffix, every model call returns `404 Not Found`. ```sh - export AGENTGATEWAY_URL=http://agentgateway-proxy.agentgateway-system.svc.cluster.local + export AGENTGATEWAY_URL=http://agentgateway-proxy.agentgateway-system.svc.cluster.local/v1 ``` ## Connect to an agentgateway endpoint {#connect} @@ -90,7 +103,7 @@ You can route model requests through an [agentgateway](https://agentgateway.dev/ | --- | --- | | `provider` | Set to `OpenAI`, because agentgateway exposes an OpenAI-compatible API. | | `model` | The model name to request from agentgateway. This value must match the name of an `AgentgatewayModel` resource in your agentgateway deployment. | - | `openAI.baseUrl` | The Kubernetes service address of your agentgateway Gateway, set in `$AGENTGATEWAY_URL`. | + | `openAI.baseUrl` | The Kubernetes service address of your agentgateway Gateway, including the `/v1` path, set in `$AGENTGATEWAY_URL`. | {{% /tab %}} {{% tab name="API key authentication" %}} If your agentgateway deployment applies an `apiKeyAuthentication` policy with `mode: Strict`, you must provide an API key in the `ModelConfig`. @@ -128,7 +141,7 @@ You can route model requests through an [agentgateway](https://agentgateway.dev/ | `model` | The model name to request from agentgateway. This value must match the name of an `AgentgatewayModel` resource in your agentgateway deployment. | | `apiKeySecret` | The name of the Kubernetes secret that stores your agentgateway API key. | | `apiKeySecretKey` | The key in the secret that stores your API key. | - | `openAI.baseUrl` | The Kubernetes service address of your agentgateway Gateway, set in `$AGENTGATEWAY_URL`. | + | `openAI.baseUrl` | The Kubernetes service address of your agentgateway Gateway, including the `/v1` path, set in `$AGENTGATEWAY_URL`. | {{% /tab %}} {{< /tabs >}} diff --git a/docs-site/content/kagent/1.x/_index.md b/docs-site/content/kagent/1.x/_index.md index 69439590..acd2827b 100644 --- a/docs-site/content/kagent/1.x/_index.md +++ b/docs-site/content/kagent/1.x/_index.md @@ -1,23 +1,9 @@ --- -title: kagent 1.0 (preview) -linkTitle: "kagent 1.x (preview)" -description: Preview the kagent 1.0 documentation, built on Agent Substrate. +title: kagent +linkTitle: "kagent 1.x" +description: Explore concepts, guides, and reference docs for running declarative AI agents on Kubernetes with Agent Substrate, from install through day-two operations. weight: 10 author: kagent.dev -# The 1.x docset is withheld from the published site until the 1.0 release is -# announced. `draft` keeps this section page out of a production `hugo` build and -# the cascade applies it to every descendant, so no 1.x page, list page, or -# llms.txt entry is generated. Authoring is unaffected: `make serve-docs` passes -# -D, so the section renders locally exactly as it will on release. -# -# To publish: delete both keys here, and restore the "1.x" entry in -# params.sections.kagent.versions in hugo.yaml, which is what puts the version -# back in the switcher. -draft: true -cascade: - draft: true --- -kagent 1.0 moves the runtime from Kubernetes Deployments to [Agent Substrate](https://github.com/agent-substrate/substrate), introducing Harness, AgentTemplate, and AgentInstance as the new API surface. For a summary of what changed in 1.0, see [Release notes]({{< link path="reference/release-notes/1.0#100" >}}). - -This section is under active development ahead of the 1.0 release. Pages here may be incomplete, and content may change without notice. For the current stable release, see the [0.x docs]({{< relref "/kagent/0.x" >}}). +kagent 1.0 moves the runtime from Kubernetes Deployments to [Agent Substrate](https://github.com/agent-substrate/substrate), introducing Harness, AgentTemplate, and AgentInstance as the new API surface. For a summary of what changed in 1.0, see the [Release notes]({{< link path="reference/release-notes/1.0#100" >}}). \ No newline at end of file diff --git a/docs-site/content/kagent/1.x/agents/agent-harness.md b/docs-site/content/kagent/1.x/agents/agent-harness.md index 95aaec04..7b628a92 100644 --- a/docs-site/content/kagent/1.x/agents/agent-harness.md +++ b/docs-site/content/kagent/1.x/agents/agent-harness.md @@ -22,7 +22,7 @@ spec: # Exactly one runtime block: kagent, codex, claude, or byo. kagent: {} workload: - image: @sha256: + image: {{< reuse "kagent-docs/versions/runtime-image.md" >}} env: - name: LOG_LEVEL value: info @@ -130,6 +130,26 @@ The coding-agent runtimes also constrain what an AgentTemplate can ask for. The `kagent` and `byo` runtimes take the full set. For more information about what an AgentTemplate can bind, see [About tools]({{< link path="skills-and-mcp/about-tools" >}}). +## Telemetry content settings + +Tracing and audit logging both carry the prompts and replies that an agent exchanges with a model. Two settings in the kagent Helm chart decide whether that content leaves the runtime, and each one reaches a different set of runtimes. Both default to `false`, and both take effect only where tracing or audit logging is already enabled. + +```yaml +otel: + captureSensitiveContent: false + logging: + captureRawApiBodies: false +``` + +| Setting | What it includes | Applies to | +| ------- | ---------------- | ---------- | +| `otel.captureSensitiveContent` | Prompts, tool details, and assistant replies in the runtime's telemetry. On the `claude` runtime, tool results require tracing, and assistant replies require audit logging. | `codex`, `claude` | +| `otel.logging.captureRawApiBodies` | The complete provider API request and response bodies. This setting returns more than `otel.captureSensitiveContent` does, and it takes effect only when `otel.logging.enabled` is `true`. | `claude` | + +The `kagent` runtime honors neither setting. To include message content for an agent on that runtime, set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` in the Harness `spec.env` field. That variable defaults differently for each signal, so check the [audit prompt onfiguration]({{< link path="observability/audit-prompts#configuration" >}}) before you set it. + +The controller sends the `byo` runtime no telemetry configuration, so neither setting reaches it. A `byo` image that implements OpenTelemetry itself reads whatever the Harness `spec.env` field holds. For more information, see [Tracing]({{< link path="observability/tracing#about-trace-coverage" >}}). + ## Check that a Harness is ready The `READY` column reports whether a Harness's dependencies resolved. diff --git a/docs-site/content/kagent/1.x/examples/agentgateway.md b/docs-site/content/kagent/1.x/examples/agentgateway.md new file mode 100644 index 00000000..d0f2d7f9 --- /dev/null +++ b/docs-site/content/kagent/1.x/examples/agentgateway.md @@ -0,0 +1,359 @@ +--- +title: Block PII in an agent's model requests +description: Add a prompt guard to an agentgateway model so that a prompt carrying personally identifiable information never reaches the provider. +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. + +[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. + +## About prompt guards on a model + +A prompt guard inspects the body of an OpenAI-compatible request as the request passes through the gateway, and either rejects the request or masks the matched text. Because the guard runs at the gateway, no change to an {{< gloss "AgentTemplate" >}}AgentTemplate{{< /gloss >}} is needed, and every agent that shares the model inherits the guard. + +A guard can live in two places, and the choice follows the routing that you already use: + +- **On the `AgentgatewayModel`**, under `spec.policies.promptGuard`. The guard applies to that one model, and no other resource is involved. This example uses this form, because 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). + +The following `AgentgatewayModel` carries the guard that the rest of this example applies. `provider` and `parentRefs` route the model, and `policies` adds the guard. + +```yaml +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayModel +metadata: + name: gpt-4o-mini + namespace: agentgateway-system +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: agentgateway-proxy + sectionName: http + provider: OpenAI + policies: + promptGuard: + request: + - regex: + builtins: + - Email + - Ssn + - CreditCard + action: Reject + response: + message: The prompt contained personally identifiable information. +``` + +A guard needs only `regex`. The remaining fields take defaults, and those defaults are permissive, so a guard that omits `action` masks the match rather than rejecting the request. + +| Field | Description | +| ----- | ----------- | +| `policies.promptGuard.request[]` | The guards to apply to requests that the agent sends. A separate `response` list guards what the provider sends back. | +| `regex.builtins` | Built-in patterns for common personally identifiable information (PII). The five values are `Email`, `Ssn`, `CreditCard`, `PhoneNumber`, and `CaSin`. To add your own patterns, use `regex.matches`, which holds a list of regular expressions and is additive with `builtins`. | +| `regex.action` | What to do with a match, either `Reject` or `Mask`. Omit to default to `Mask`, which is also the safer choice for an agent. A `Reject` guard ends the conversation permanently, for the reason described in [Mask instead of reject](#mask-instead-of-reject). | +| `response.message` | The message that the gateway returns to the caller on a rejection. Omit to default to `The request was rejected due to inappropriate content`. A sibling `response.statusCode` field sets the status code, and defaults to `403`. | + +> [!IMPORTANT] +> **A request guard inspects the system prompt as well as the messages.** The default scope is `SystemPrompt` and `Messages`, so an AgentTemplate whose `systemPrompt` contains an example email address fails every turn rather than only the turns where a person types one. Check the system prompts of the agents that share a model before you turn on `Reject`. + +## Before you begin + +1. [Install kagent]({{< link path="setup/installation" >}}), and [create your first agent]({{< link path="get-started/your-first-agent" >}}) so that you have a {{< gloss "Harness" >}}Harness{{< /gloss >}} and know which label it admits. This example uses a Harness named `my-first-harness` that admits the label `kagent.dev/harness: my-first-harness`. + +2. Set up [agentgateway model routing]({{< link path="setup/model-providers/byo-agentgateway#set-up-agentgateway-model-routing" >}}), which installs agentgateway with `--set agentgatewayModels.enabled=true` and creates a `Gateway` named `agentgateway-proxy` and an `AgentgatewayModel` named `gpt-4o-mini`. + +3. Create the [ModelConfig]({{< link path="setup/model-providers/byo-agentgateway#create-the-modelconfig" >}}) that points at the gateway. This example uses a ModelConfig named `agentgateway-model-config` in the `kagent` namespace, and the deployment that it reaches enforces no API key authentication. + +## Add the prompt guard to the model + +Adding `policies` to a model that already routes traffic changes nothing about the routing, so you reapply the same resource with the guard attached. + +1. Reapply the `AgentgatewayModel` with a request guard that rejects three kinds of PII. + ```bash + kubectl apply -f - <}} + {{% tab name="Cloud Provider LoadBalancer" %}} + ```bash + export AGENTGATEWAY_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") + echo $AGENTGATEWAY_ADDRESS + ``` + {{% /tab %}} + {{% tab name="Port-forward for local testing" %}} + ```bash + kubectl port-forward -n agentgateway-system service/agentgateway-proxy 8080:80 + export AGENTGATEWAY_ADDRESS=localhost:8080 + ``` + {{% /tab %}} + {{< /tabs >}} + +2. Send a prompt that contains an email address. + ```bash + curl -i http://$AGENTGATEWAY_ADDRESS/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Summarize the ticket from alex@example.com"}]}' + ``` + + The gateway returns the status code and message from the guard, and the provider never receives the request. Example output: + ```console + HTTP/1.1 403 Forbidden + + The prompt contained personally identifiable information. + ``` + +3. Send a prompt that carries no PII, to confirm that ordinary traffic still reaches the provider. + ```bash + curl -i http://$AGENTGATEWAY_ADDRESS/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Summarize the most recent ticket"}]}' + ``` + + The response is an ordinary completion. Example output, truncated: + ```console + HTTP/1.1 200 OK + content-type: application/json + + {"model":"gpt-4o-mini-2024-07-18","usage":{"prompt_tokens":14,"completion_tokens":33,...},"choices":[{"message":{"content":"I'm sorry, but I don't have access to specific ... + ``` + +4. Create an agent that uses the guarded model, and an {{< gloss "AgentInstance" >}}AgentInstance{{< /gloss >}} to talk to it. + ```bash + kubectl apply -f - < [!NOTE] +> **The guard's `response.message` does not reach the person talking to the agent.** A caller who reads the gateway's response directly sees the message, as the curl steps show, but kagent surfaces only the status code and the failed task. Treat `response.message` as something for an operator reading gateway logs rather than as an explanation for the end user, and put the explanation your users need in the agent's system prompt instead. + +> [!CAUTION] +> **A rejected turn strands the conversation permanently.** An agent is not a single-shot chat client: the {{< gloss "Transcript" >}}transcript{{< /gloss >}} is append-only, and a failed turn does not remove the message that failed. The AgentInstance resends the whole conversation on every turn, so the prompt that tripped the guard is sent again, and rejected again, for the rest of that AgentInstance's life. +> +> The following sequence reproduces it. A fresh AgentInstance answers `Summarize the most recent ticket` normally. Send `Summarize the ticket from alex@example.com` next, and that turn fails with the `403`. Then send `What is 2+2?`, which carries no PII of its own: that turn fails with a `403` as well, and so does every turn after it. +> +> Creating a new AgentInstance is the only way to recover a conversation that a `Reject` guard has stopped. For agents, that cost is the strongest argument for [masking instead](#mask-instead-of-reject). + +## Mask instead of reject + +Masking is the better default for an agent, for the reason the preceding section demonstrates: a `Reject` guard ends the conversation for good, while a `Mask` guard lets the turn through with the matched text replaced. Reserve `Reject` for a policy that forbids PII outright and accepts a dead conversation as the price. + +Masking also repairs a conversation that a `Reject` guard already stopped. Changing the action re-masks the offending message on the next turn rather than rejecting it, so the stranded AgentInstance answers again without being recreated. + +1. Change the action on the request guard to `Mask`. + ```bash + kubectl patch agentgatewaymodel gpt-4o-mini -n agentgateway-system --type merge -p \ + '{"spec":{"policies":{"promptGuard":{"request":[{"regex":{"builtins":["Email","Ssn","CreditCard"],"action":"Mask"}}]}}}}' + ``` + +2. Send the same prompt again, to the same AgentInstance that the `Reject` guard stranded. + ```bash + kagent invoke --agent-instance $INSTANCE_ID \ + --task "Summarize this ticket: alex@example.com reports that checkout returns 503 errors during peak hours." + ``` + + The turn succeeds this time. The agent answers, or asks a follow-up question, and the address never reaches the provider. The patch replaces the whole `request` list, so it also drops the `response.message` that the rejection used; a `Mask` guard returns no message, because it rejects nothing. + +> [!WARNING] +> **A guard on the response does not inspect streamed content unless you enable it.** Prompt guards default to skipping streaming responses to preserve throughput. Set `policies.promptGuard.streaming: Enabled` to guard them, and note that `Mask` is never applied to a streamed response even then: a guard can reject streamed content, and matched text in a stream that is not rejected passes through unmodified. + +## Guard what an agent's tools send + +An agent differs from a chat client in that a good deal of its traffic originates from tools rather than from a person. A tool that reads a ticket, a database row, or a Kubernetes resource can feed PII back to the model, and the default scope does not inspect that content. + +Add `scope` to the guard to cover tool traffic. `ToolOutput` covers the results that a tool feeds back to the model, and `ToolInput` covers the arguments that the model produces for a tool call. + +```yaml + policies: + promptGuard: + request: + - scope: + - SystemPrompt + - Messages + - ToolOutput + regex: + builtins: + - Email + - Ssn + - CreditCard + action: Mask +``` + +> [!NOTE] +> Listing `scope` replaces the default rather than adding to it, so name `SystemPrompt` and `Messages` alongside the tool scopes to keep guarding the conversation. In an API that sends tool arguments as opaque JSON, a `Mask` action on `ToolInput` can rewrite those arguments into invalid JSON, so prefer `ToolOutput` unless you have a reason to inspect the arguments. + +## Attach the guard to a route instead + +An `AgentgatewayPolicy` holds the same `promptGuard` configuration, and one policy can govern every model behind a route rather than one model at a time. Because `targetRefs` accepts an `HTTPRoute` and not an `AgentgatewayModel`, this form needs the model to attach to a route instead of to the listener. + +1. Create an `HTTPRoute` whose rule sends traffic to every model, and point the model at the route rather than at the `Gateway`. + ```bash + kubectl apply -f - < [!NOTE] +> The listener must allow the `HTTPRoute` kind for this variant. The Gateway that [agentgateway model routing]({{< link path="setup/model-providers/byo-agentgateway#set-up-agentgateway-model-routing" >}}) creates already allows both `HTTPRoute` and `AgentgatewayModel`, so no change to the Gateway is needed. + +## Clean up + +1. Delete the AgentInstance and the AgentTemplate. + ```bash + kagent delete agent-instance $INSTANCE_ID + kubectl delete agenttemplate support-triage -n kagent + ``` + +2. Remove the guard from the model, leaving the routing and the provider credentials in place. The path names `promptGuard` rather than `policies`, because `policies` also holds the `auth` that lets the gateway reach the provider. + ```bash + kubectl patch agentgatewaymodel gpt-4o-mini -n agentgateway-system --type json -p '[{"op":"remove","path":"/spec/policies/promptGuard"}]' + ``` + +3. If you followed [Attach the guard to a route instead](#attach-the-guard-to-a-route-instead), delete the policy and the route, and repoint the model at the Gateway listener. + ```bash + kubectl delete agentgatewaypolicy block-pii -n agentgateway-system + kubectl delete httproute model-traffic -n agentgateway-system + ``` + +## Next steps + +{{< cards >}} + {{< card link=`{{< link path="setup/model-providers/byo-agentgateway" >}}` title="agentgateway model routing" subtitle="Review the Gateway, AgentgatewayModel, and ModelConfig that carry an agent's model traffic." >}} + {{< card link=`{{< link path="observability/audit-prompts" >}}` title="Audit prompts" subtitle="Export the prompts and replies that your agents exchange with a model, and query them in a logging backend." >}} + {{< card link="https://agentgateway.dev/docs/kubernetes/latest/documentation/llm/guardrails/regex/#block-requests-with-pii" title="agentgateway regex guardrails" subtitle="Read the full set of regex guards, masking rules, and built-in patterns in the agentgateway documentation." >}} +{{< /cards >}} diff --git a/docs-site/content/kagent/1.x/examples/agents-via-mcp.md b/docs-site/content/kagent/1.x/examples/agents-via-mcp.md index 3d6742a6..627be4b4 100644 --- a/docs-site/content/kagent/1.x/examples/agents-via-mcp.md +++ b/docs-site/content/kagent/1.x/examples/agents-via-mcp.md @@ -411,8 +411,8 @@ When an agent pauses to ask something, the task's status becomes `input_required } ``` -> [!WARNING] -> A response whose key does not match the `inputRequests` key is discarded silently. The call still returns `{ "resultType": "complete" }`, but the agent never receives the answer and the task stays `input_required` until something answers it under the right key or cancels it. Read the key from `tasks/get` rather than reusing a task ID or a checkpoint ID. +> [!NOTE] +> A response whose key does not match the `inputRequests` key is rejected with an `Invalid params` error (`-32602`) naming the key that the task expects, and the task stays `input_required`. A value that is not an elicitation result is rejected the same way. Read the key from `tasks/get` rather than reusing a task ID or a checkpoint ID. For the pause types, the approval model, and what the agent receives, see [Human in the loop]({{< link path="agents/human-in-the-loop" >}}). diff --git a/docs-site/content/kagent/1.x/get-started/your-first-agent.md b/docs-site/content/kagent/1.x/get-started/your-first-agent.md index d92b0559..fbbcbf2f 100644 --- a/docs-site/content/kagent/1.x/get-started/your-first-agent.md +++ b/docs-site/content/kagent/1.x/get-started/your-first-agent.md @@ -32,8 +32,8 @@ This guide walks you through creating an agent, from applying a Harness and an A spec: kagent: {} workload: - # Your kagent release's runtime image - image: @sha256: + # kagent's native runtime image, pinned by digest + image: {{< reuse "kagent-docs/versions/runtime-image.md" >}} substrate: workerPoolRef: name: kagent-default diff --git a/docs-site/content/kagent/1.x/observability/audit-prompts.md b/docs-site/content/kagent/1.x/observability/audit-prompts.md index 8d19d531..4da3e65b 100644 --- a/docs-site/content/kagent/1.x/observability/audit-prompts.md +++ b/docs-site/content/kagent/1.x/observability/audit-prompts.md @@ -30,15 +30,21 @@ An audit returns more than the prompts that your team wrote. A `gen_ai.system.me > [!NOTE] > The runtime labels every history entry as `gen_ai.user.message`, including the agent's own earlier turns and tool results. The `content.role` field in the event body names the speaker. To select only the messages that a person sent, filter on `content.role` instead of on the event name. Each turn also re-emits the full history, so a long conversation produces repeated events. Account for that volume when you set a retention period. -### Environment variables +### Configuration -Two environment variables on the agent runtime control the audit output. +Audit output comes from two places. The kagent Helm chart decides whether the runtime exports events and where it sends them. The {{< gloss "Harness" >}}Harness{{< /gloss >}} decides whether those events carry message content. -- **`OTEL_LOGGING_ENABLED`**: Whether the runtime installs a log exporter. The default value is `false`, and the runtime then emits no audit events. -- **`OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`**: Whether the events include message content. The default value for log events is `false`, and the runtime then replaces each message body with ``. The event metadata remains. +| Setting | Where you set it | What it does | +| ------- | ---------------- | ------------ | +| `otel.logging.enabled` | kagent Helm chart | Installs the log exporter in the agent runtime. The default value is `false`, and the runtime then emits no audit events, regardless of the other settings. | +| `otel.logging.exporter.otlp.endpoint` | kagent Helm chart | The address that the runtime exports events to. Set it to the address of your collector. | +| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | `Harness.spec.env` | Includes message content in the events. The default value is `false`, and the runtime then replaces each message body with ``. The event metadata and the trace IDs remain. Those fields still record which agent handled a request, and when. | > [!IMPORTANT] -> The `otel.logging` settings in the kagent Helm chart configure the controller only. The controller passes a fixed list of tracing variables to the agent runtimes that it starts. That list holds no logging variable. As a result, `otel.logging.enabled=true` alone produces no audit events for an AgentInstance. Set the logging variables on the {{< gloss "Harness" >}}Harness{{< /gloss >}} instead, as shown in the following steps. A Harness `spec.env` entry for a logging variable takes effect as written. An entry for a tracing variable does not, because the controller's own value overrides it. +> The chart's `otel.captureSensitiveContent` setting does not reach the `kagent` runtime. It applies to the Claude and Codex runtimes only. To include message content in an audit of a `kagent` agent, set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` on the Harness, as shown in the following steps. + +> [!NOTE] +> The controller compiles the chart's logging settings into every runtime revision, and its values override a Harness `spec.env` entry for the same variable. The variables it owns are `OTEL_LOGGING_ENABLED`, `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`, and `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`, together with their tracing equivalents and the endpoint and protocol variables that cover both signals. If `otel.logging.enabled` is `false`, the controller compiles no logging variable, and a `spec.env` entry takes effect as written. ### Runtime support @@ -201,9 +207,21 @@ Export to a collector rather than directly to the backend. The collector holds t ## Turn on audit logging -Add the audit variables to the Harness that your agents run on. The runtime then audits every AgentTemplate that the Harness admits. Auditing therefore applies to an entire Harness, not to a single agent. +Turning on auditing takes two changes. The chart setting installs the log exporter in every agent runtime that the controller starts, and the Harness setting decides whether the exported events carry message content. A Harness applies to every AgentTemplate that it admits, so auditing covers an entire Harness rather than a single agent. + +1. Upgrade kagent to export audit events to the collector. The controller compiles these settings into every runtime revision that it builds from now on. + ```bash + helm upgrade kagent \ + oci://ghcr.io/kagent-dev/kagent/helm/kagent \ + --version {{< reuse "kagent-docs/versions/kagent.md" >}} \ + --namespace kagent --reuse-values \ + --set otel.logging.enabled=true \ + --set otel.logging.exporter.otlp.endpoint=http://opentelemetry-collector-audit.telemetry.svc.cluster.local:4317 + ``` + + To export over HTTP instead of gRPC, add `--set otel.logging.exporter.otlp.protocol=http/protobuf` and use port `4318`. -1. Add the three environment variables to the Harness. Keep the rest of its configuration unchanged. +2. Add the message content variable to the Harness. Keep the rest of its configuration unchanged. ```yaml kubectl apply -f - <@sha256: + image: {{< reuse "kagent-docs/versions/runtime-image.md" >}} env: - - name: OTEL_LOGGING_ENABLED - value: "true" - - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT - value: http://opentelemetry-collector-audit.telemetry.svc.cluster.local:4317 - name: OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT value: "true" substrate: @@ -234,15 +248,9 @@ Add the audit variables to the Harness that your agents run on. The runtime then EOF ``` - {{< reuse "kagent-docs/snippets/review-table.md" >}} The first two variables enable auditing and set its destination. The third variable controls whether the events include message content. For every other field that a Harness takes, see [Agent harness]({{< link path="agents/agent-harness" >}}). - - | Variable | Description | - | -------- | ----------- | - | `OTEL_LOGGING_ENABLED` | Installs the log exporter in the agent runtime. If omitted, the runtime emits no audit events, regardless of the other variables. | - | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | The address that the runtime exports events to. Set it to the address of the collector. To export over HTTP instead of gRPC, set `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` to `http/protobuf` and use port `4318`. | - | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | Includes message content in the events. If omitted, each event body reads ``, and the runtime exports only the metadata and the trace IDs. Those fields still record which agent handled a request, and when. | + The `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` variable includes message content in the events. If you omit it, each event body reads ``, and the runtime exports only the metadata and the trace IDs. Those fields still record which agent handled a request, and when. Set the export destination through the chart rather than here, because the controller's compiled values override a `spec.env` entry for a variable that it owns. For every other field that a Harness takes, see [Agent harness]({{< link path="agents/agent-harness" >}}). -2. Confirm that kagent compiled a new {{< gloss "Revision" >}}revision{{< /gloss >}} for the edited Harness. The Harness is current when `latestSuccessfulRevision` matches `desiredRevision`. +3. Confirm that kagent compiled a new {{< gloss "Revision" >}}revision{{< /gloss >}} for the edited Harness. The Harness is current when `latestSuccessfulRevision` matches `desiredRevision`. ```bash kubectl get agenttemplate my-first-agent -n kagent \ -o jsonpath='{range .status.harnesses[*]}{.harness}{"\t"}{.desiredRevision}{"\t"}{.latestSuccessfulRevision}{"\n"}{end}' @@ -253,7 +261,7 @@ Add the audit variables to the Harness that your agents run on. The runtime then my-first-harness 4b8e1d3f5a7c9e2b0d4f6a8c1e3b5d7f9a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b 4b8e1d3f5a7c9e2b0d4f6a8c1e3b5d7f9a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b ``` -3. Create a new AgentInstance. An {{< gloss "AgentInstance" >}}AgentInstance{{< /gloss >}} pins the revision that it was created from, so an existing instance continues to run without the audit variables. +4. Create a new AgentInstance. An {{< gloss "AgentInstance" >}}AgentInstance{{< /gloss >}} pins the revision that it was created from, so an existing instance continues to run without auditing. ```bash kagent create agent-instance --harness my-first-harness --agent-template my-first-agent ``` @@ -368,11 +376,20 @@ To follow a request from its audit records into its trace, take the `trace_id` f ## Turn off audit logging -1. Remove the three environment variables from the `spec.env` field of the Harness. +1. Turn the log exporter off again. + ```bash + helm upgrade kagent \ + oci://ghcr.io/kagent-dev/kagent/helm/kagent \ + --version {{< reuse "kagent-docs/versions/kagent.md" >}} \ + --namespace kagent --reuse-values \ + --set otel.logging.enabled=false + ``` + +2. Remove the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` variable from the `spec.env` field of the Harness. -2. Create a new AgentInstance, so that its Actor starts without the audit variables. +3. Create a new AgentInstance, so that its Actor starts without auditing. -3. Remove the collector and the logging backend. +4. Remove the collector and the logging backend. ```bash helm uninstall opentelemetry-collector-audit -n telemetry helm uninstall loki -n telemetry diff --git a/docs-site/content/kagent/1.x/observability/launch-ui.md b/docs-site/content/kagent/1.x/observability/launch-ui.md index 94ca62a7..40760351 100644 --- a/docs-site/content/kagent/1.x/observability/launch-ui.md +++ b/docs-site/content/kagent/1.x/observability/launch-ui.md @@ -14,14 +14,14 @@ The kagent UI is a read-and-write console for everything the controller knows ab 2. Confirm that the UI service exists. ```bash - kubectl get svc -n kagent kagent-ui + kubectl get svc -n kagent {{< reuse "kagent-docs/snippets/name-ui.md" >}} ``` Example output: ```console NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE - kagent-ui ClusterIP 10.96.174.203 8080/TCP 3m + {{< reuse "kagent-docs/snippets/name-ui.md" >}} ClusterIP 10.96.174.203 8080/TCP 3m ``` ## Open the dashboard @@ -49,10 +49,10 @@ The UI service is a `ClusterIP` by default, so it is reachable only from inside > `kagent dashboard` opens a browser only on macOS. On Linux and Windows the command prints the `kubectl` port-forward command and exits, so follow the port-forward steps instead. {{% /tab %}} {{% tab name="Port-forward for local testing" %}} -1. Forward the `kagent-ui` service to your machine, and leave the command running. +1. Forward the `{{< reuse "kagent-docs/snippets/name-ui.md" >}}` service to your machine, and leave the command running. ```bash - kubectl port-forward -n kagent service/kagent-ui 8082:8080 + kubectl port-forward -n kagent service/{{< reuse "kagent-docs/snippets/name-ui.md" >}} 8082:8080 ``` 2. Open [http://localhost:8082](http://localhost:8082) in your browser. @@ -141,7 +141,7 @@ ui: Then read the external address from the service. ```bash -kubectl get svc -n kagent kagent-ui +kubectl get svc -n kagent {{< reuse "kagent-docs/snippets/name-ui.md" >}} ``` ### OpenShift route diff --git a/docs-site/content/kagent/1.x/observability/tracing.md b/docs-site/content/kagent/1.x/observability/tracing.md index cf2f984d..da2141ca 100644 --- a/docs-site/content/kagent/1.x/observability/tracing.md +++ b/docs-site/content/kagent/1.x/observability/tracing.md @@ -36,7 +36,10 @@ flowchart LR A caller reaches the gRPC API on the kagent controller, which starts the trace. The controller hands the request to its A2A gateway, which opens an {{< gloss "A2A" >}}A2A{{< /gloss >}} (Agent-to-Agent) connection to the AgentInstance's Actor and injects a `traceparent` header into that call. The agent runtime inside the Actor reads the header and continues the same trace, so the model and tool spans it produces hang off the controller's spans rather than starting a trace of their own. > [!IMPORTANT] -> The controller passes its tracing configuration only to the `kagent` runtime. An agent on the `codex`, `claude`, or `byo` runtime produces no runtime spans, and its half of the trace is missing. For a `byo` image that implements OTel itself, set the exporter variables in the Harness `spec.env` instead. For the available runtimes, see [Choose a runtime]({{< link path="agents/agent-harness#choose-a-runtime" >}}). +> 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" >}}). Both processes report themselves as separate OpenTelemetry (OTel) services. A tracing backend uses these service names to group the spans. @@ -48,7 +51,7 @@ Both processes report themselves as separate OpenTelemetry (OTel) services. A tr ### Spans -The agent runtime creates the same spans for every agent, and most span names describe the operation rather than the agent. The `invoke_agent` span is the exception, because its name carries the service name of the agent that ran. To narrow a search to one agent, filter by service name rather than by span name. The following spans appear in nesting order, from the span that accepts the request down to the model and tool calls that serve it. +The `kagent` runtime creates the same spans for every agent, and most span names describe the operation rather than the agent. The `invoke_agent` span is the exception, because its name carries the service name of the agent that ran. To narrow a search to one agent, filter by service name rather than by span name. The following spans appear in nesting order, from the span that accepts the request down to the model and tool calls that serve it. | Span | When it is created | | ---- | ------------------ | @@ -73,7 +76,7 @@ A trace tells you which request you are looking at through attributes on its spa The runtime also adds each scalar value in the A2A message's metadata as an `a2a.message.metadata.` attribute, so a client can tag a request and search for it later. Unlike the four correlation attributes, these tags stay on the `invocation` span alone, so a search on one returns that span instead of the whole subtree. > [!WARNING] -> Spans for a model call carry the full serialized request and response as the `gcp.vertex.agent.llm_request` and `gcp.vertex.agent.llm_response` attributes. Prompts and replies therefore reach your tracing backend. Payloads larger than 32 KiB are truncated to a prefix. To keep this content out of traces, set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to `false` in the Harness `spec.env`. Note that the same variable has the opposite default for audit logging, where content is withheld until you set it to `true`. For more information, see [Audit prompts]({{< link path="observability/audit-prompts" >}}). +> Spans for a model call carry the full serialized request and response as the `gcp.vertex.agent.llm_request` and `gcp.vertex.agent.llm_response` attributes. Prompts and replies therefore reach your tracing backend. Payloads larger than 32 KiB are truncated to a prefix. To keep this content out of traces, set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` to `false` in the Harness `spec.env`. Note that the same variable has the opposite default for audit logging, where content is withheld until you set it to `true`. For more information, see [Audit prompts]({{< link path="observability/audit-prompts" >}}). The `codex` and `claude` runtimes do not read this variable. On those two runtimes, the `otel.captureSensitiveContent` Helm setting controls the same content, as described in the agent harness [telemetry content settings]({{< link path="agents/agent-harness#telemetry-content-settings" >}}). ## Before you begin @@ -187,7 +190,7 @@ Tracing is off by default. Turning it on is a Helm change, because the controlle Agent Substrate {{< gloss "Checkpoint" >}}checkpoints{{< /gloss >}} an Actor as soon as the response body closes, which is sooner than a batching span exporter normally sends its buffer. Spans still in the buffer at that moment freeze inside the {{< gloss "Snapshot" >}}snapshot{{< /gloss >}} and reach the backend only when the session next resumes, or never at all for a conversation's last message. -To avoid losing them, the controller sets `KAGENT_PRE_RESPONSE_TRACE_FLUSH` to `true` on every agent runtime it starts, and the runtime flushes its span buffer before each response completes. The flush waits up to three seconds, which you can change with `KAGENT_TRACE_FLUSH_TIMEOUT_MS` in the Harness `spec.env`. +To avoid losing them, the controller sets `KAGENT_PRE_RESPONSE_TRACE_FLUSH` to `true` on the `kagent` and `codex` runtimes, and the runtime flushes its span buffer before each response completes. The flush waits up to three seconds, which you can change with `KAGENT_TRACE_FLUSH_TIMEOUT_MS` in the Harness `spec.env`. The `claude` runtime gets no such flush, so its spans arrive on its exporter's own schedule and a conversation's last turn can lose them. This behavior allows a kagent trace to arrive promptly rather than on the exporter's own schedule. To understand what suspension does to an Actor, see [Suspend and resume]({{< link path="substrate-runtime/suspend-and-resume" >}}). diff --git a/docs-site/content/kagent/1.x/reference/cli/_index.md b/docs-site/content/kagent/1.x/reference/cli/_index.md index 399a0fcf..05713a77 100644 --- a/docs-site/content/kagent/1.x/reference/cli/_index.md +++ b/docs-site/content/kagent/1.x/reference/cli/_index.md @@ -6,16 +6,16 @@ weight: 30 Review the kagent CLI commands and learn how to use them effectively. -- [`kagent apply`](/docs/kagent/1.x/reference/cli/kagent-apply/) - Create or update an AgentTemplate -- [`kagent bug-report`](/docs/kagent/1.x/reference/cli/kagent-bug-report/) - Generate a bug report -- [`kagent completion`](/docs/kagent/1.x/reference/cli/kagent-completion/) - Generate the autocompletion script for the specified shell -- [`kagent create`](/docs/kagent/1.x/reference/cli/kagent-create/) - Create a kagent resource -- [`kagent dashboard`](/docs/kagent/1.x/reference/cli/kagent-dashboard/) - Open the kagent dashboard -- [`kagent db`](/docs/kagent/1.x/reference/cli/kagent-db/) - Database operations (migrations, inspection) -- [`kagent delete`](/docs/kagent/1.x/reference/cli/kagent-delete/) - Delete a kagent resource -- [`kagent get`](/docs/kagent/1.x/reference/cli/kagent-get/) - Get a kagent resource -- [`kagent install`](/docs/kagent/1.x/reference/cli/kagent-install/) - Install kagent -- [`kagent invoke`](/docs/kagent/1.x/reference/cli/kagent-invoke/) - Invoke an AgentInstance -- [`kagent mcp`](/docs/kagent/1.x/reference/cli/kagent-mcp/) - MCP (Model Context Protocol) server management -- [`kagent uninstall`](/docs/kagent/1.x/reference/cli/kagent-uninstall/) - Uninstall kagent -- [`kagent version`](/docs/kagent/1.x/reference/cli/kagent-version/) - Print the kagent version +- [`kagent apply`]({{< link path="reference/cli/kagent-apply" >}}) - Create or update an AgentTemplate +- [`kagent bug-report`]({{< link path="reference/cli/kagent-bug-report" >}}) - Generate a bug report +- [`kagent completion`]({{< link path="reference/cli/kagent-completion" >}}) - Generate the autocompletion script for the specified shell +- [`kagent create`]({{< link path="reference/cli/kagent-create" >}}) - Create a kagent resource +- [`kagent dashboard`]({{< link path="reference/cli/kagent-dashboard" >}}) - Open the kagent dashboard +- [`kagent db`]({{< link path="reference/cli/kagent-db" >}}) - Database operations (migrations, inspection) +- [`kagent delete`]({{< link path="reference/cli/kagent-delete" >}}) - Delete a kagent resource +- [`kagent get`]({{< link path="reference/cli/kagent-get" >}}) - Get a kagent resource +- [`kagent install`]({{< link path="reference/cli/kagent-install" >}}) - Install kagent +- [`kagent invoke`]({{< link path="reference/cli/kagent-invoke" >}}) - Invoke an AgentInstance +- [`kagent mcp`]({{< link path="reference/cli/kagent-mcp" >}}) - MCP (Model Context Protocol) server management +- [`kagent uninstall`]({{< link path="reference/cli/kagent-uninstall" >}}) - Uninstall kagent +- [`kagent version`]({{< link path="reference/cli/kagent-version" >}}) - Print the kagent version diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-completion.md b/docs-site/content/kagent/1.x/reference/cli/kagent-completion.md index 01592caf..9e97ccdc 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-completion.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-completion.md @@ -12,10 +12,10 @@ kagent completion [command] ``` **Subcommands:** -- [`kagent completion bash`](/docs/kagent/1.x/reference/cli/kagent-completion-bash/) - Generate the autocompletion script for bash -- [`kagent completion fish`](/docs/kagent/1.x/reference/cli/kagent-completion-fish/) - Generate the autocompletion script for fish -- [`kagent completion powershell`](/docs/kagent/1.x/reference/cli/kagent-completion-powershell/) - Generate the autocompletion script for powershell -- [`kagent completion zsh`](/docs/kagent/1.x/reference/cli/kagent-completion-zsh/) - Generate the autocompletion script for zsh +- [`kagent completion bash`]({{< link path="reference/cli/kagent-completion-bash" >}}) - Generate the autocompletion script for bash +- [`kagent completion fish`]({{< link path="reference/cli/kagent-completion-fish" >}}) - Generate the autocompletion script for fish +- [`kagent completion powershell`]({{< link path="reference/cli/kagent-completion-powershell" >}}) - Generate the autocompletion script for powershell +- [`kagent completion zsh`]({{< link path="reference/cli/kagent-completion-zsh" >}}) - Generate the autocompletion script for zsh **Flags:** - `-h, --help` - help for completion diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-create.md b/docs-site/content/kagent/1.x/reference/cli/kagent-create.md index f4155beb..4475f2d5 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-create.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-create.md @@ -12,7 +12,7 @@ kagent create [command] ``` **Subcommands:** -- [`kagent create agent-instance`](/docs/kagent/1.x/reference/cli/kagent-create-agent-instance/) - Create an AgentInstance +- [`kagent create agent-instance`]({{< link path="reference/cli/kagent-create-agent-instance" >}}) - Create an AgentInstance **Flags:** - `-h, --help` - help for create diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-db-migrate.md b/docs-site/content/kagent/1.x/reference/cli/kagent-db-migrate.md index 883d083a..27959670 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-db-migrate.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-db-migrate.md @@ -12,11 +12,11 @@ kagent db migrate [command] ``` **Subcommands:** -- [`kagent db migrate down`](/docs/kagent/1.x/reference/cli/kagent-db-migrate-down/) - Roll back the latest N migrations -- [`kagent db migrate goto`](/docs/kagent/1.x/reference/cli/kagent-db-migrate-goto/) - Move one source to version V -- [`kagent db migrate status`](/docs/kagent/1.x/reference/cli/kagent-db-migrate-status/) - Show migration status -- [`kagent db migrate up`](/docs/kagent/1.x/reference/cli/kagent-db-migrate-up/) - Apply all pending migrations -- [`kagent db migrate version`](/docs/kagent/1.x/reference/cli/kagent-db-migrate-version/) - Show the applied migration version +- [`kagent db migrate down`]({{< link path="reference/cli/kagent-db-migrate-down" >}}) - Roll back the latest N migrations +- [`kagent db migrate goto`]({{< link path="reference/cli/kagent-db-migrate-goto" >}}) - Move one source to version V +- [`kagent db migrate status`]({{< link path="reference/cli/kagent-db-migrate-status" >}}) - Show migration status +- [`kagent db migrate up`]({{< link path="reference/cli/kagent-db-migrate-up" >}}) - Apply all pending migrations +- [`kagent db migrate version`]({{< link path="reference/cli/kagent-db-migrate-version" >}}) - Show the applied migration version **Flags:** - `--db-url string` - PostgreSQL connection URL diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-db.md b/docs-site/content/kagent/1.x/reference/cli/kagent-db.md index 5b202454..c4e5ab65 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-db.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-db.md @@ -11,7 +11,7 @@ kagent db [command] ``` **Subcommands:** -- [`kagent db migrate`](/docs/kagent/1.x/reference/cli/kagent-db-migrate/) - Apply, roll back, and inspect database migrations +- [`kagent db migrate`]({{< link path="reference/cli/kagent-db-migrate" >}}) - Apply, roll back, and inspect database migrations **Flags:** - `-h, --help` - help for db diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-delete.md b/docs-site/content/kagent/1.x/reference/cli/kagent-delete.md index 19432097..0a42734f 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-delete.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-delete.md @@ -12,7 +12,7 @@ kagent delete [command] ``` **Subcommands:** -- [`kagent delete agent-instance`](/docs/kagent/1.x/reference/cli/kagent-delete-agent-instance/) - Delete an AgentInstance +- [`kagent delete agent-instance`]({{< link path="reference/cli/kagent-delete-agent-instance" >}}) - Delete an AgentInstance **Flags:** - `-h, --help` - help for delete diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-get.md b/docs-site/content/kagent/1.x/reference/cli/kagent-get.md index 2a4d5023..1af45371 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-get.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-get.md @@ -12,8 +12,8 @@ kagent get [command] ``` **Subcommands:** -- [`kagent get agent-instance`](/docs/kagent/1.x/reference/cli/kagent-get-agent-instance/) - Get an AgentInstance or list your AgentInstances -- [`kagent get agent-template`](/docs/kagent/1.x/reference/cli/kagent-get-agent-template/) - Get an AgentTemplate or list AgentTemplates +- [`kagent get agent-instance`]({{< link path="reference/cli/kagent-get-agent-instance" >}}) - Get an AgentInstance or list your AgentInstances +- [`kagent get agent-template`]({{< link path="reference/cli/kagent-get-agent-template" >}}) - Get an AgentTemplate or list AgentTemplates **Flags:** - `-h, --help` - help for get diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-deploy.md b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-deploy.md index 16164e0a..ede25e24 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-deploy.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-deploy.md @@ -28,7 +28,7 @@ kagent mcp deploy [command] ``` **Subcommands:** -- [`kagent mcp deploy package`](/docs/kagent/1.x/reference/cli/kagent-mcp-deploy-package/) - Deploy an MCP server using a package manager (npx, uvx) +- [`kagent mcp deploy package`]({{< link path="reference/cli/kagent-mcp-deploy-package" >}}) - Deploy an MCP server using a package manager (npx, uvx) **Flags:** - `--args strings` - Command arguments diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-init.md b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-init.md index 9685df82..bcdcdf4d 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-init.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-init.md @@ -15,10 +15,10 @@ kagent mcp init [command] ``` **Subcommands:** -- [`kagent mcp init go`](/docs/kagent/1.x/reference/cli/kagent-mcp-init-go/) - Initialize a new Go MCP server project -- [`kagent mcp init java`](/docs/kagent/1.x/reference/cli/kagent-mcp-init-java/) - Initialize a new Java MCP server project -- [`kagent mcp init python`](/docs/kagent/1.x/reference/cli/kagent-mcp-init-python/) - Initialize a new Python MCP server project -- [`kagent mcp init typescript`](/docs/kagent/1.x/reference/cli/kagent-mcp-init-typescript/) - Initialize a new TypeScript MCP server project +- [`kagent mcp init go`]({{< link path="reference/cli/kagent-mcp-init-go" >}}) - Initialize a new Go MCP server project +- [`kagent mcp init java`]({{< link path="reference/cli/kagent-mcp-init-java" >}}) - Initialize a new Java MCP server project +- [`kagent mcp init python`]({{< link path="reference/cli/kagent-mcp-init-python" >}}) - Initialize a new Python MCP server project +- [`kagent mcp init typescript`]({{< link path="reference/cli/kagent-mcp-init-typescript" >}}) - Initialize a new TypeScript MCP server project **Flags:** - `--author string` - Author name for the project diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-secrets.md b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-secrets.md index 86a91762..ac418b28 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-secrets.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp-secrets.md @@ -11,7 +11,7 @@ kagent mcp secrets [command] ``` **Subcommands:** -- [`kagent mcp secrets sync`](/docs/kagent/1.x/reference/cli/kagent-mcp-secrets-sync/) - Sync secrets to a Kubernetes environment from a local .env file +- [`kagent mcp secrets sync`]({{< link path="reference/cli/kagent-mcp-secrets-sync" >}}) - Sync secrets to a Kubernetes environment from a local .env file **Flags:** - `-h, --help` - help for secrets diff --git a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp.md b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp.md index 1ba69154..a4eaeddc 100644 --- a/docs-site/content/kagent/1.x/reference/cli/kagent-mcp.md +++ b/docs-site/content/kagent/1.x/reference/cli/kagent-mcp.md @@ -12,12 +12,12 @@ kagent mcp [command] ``` **Subcommands:** -- [`kagent mcp add-tool`](/docs/kagent/1.x/reference/cli/kagent-mcp-add-tool/) - Add a new MCP tool to your project -- [`kagent mcp build`](/docs/kagent/1.x/reference/cli/kagent-mcp-build/) - Build MCP server as a Docker image -- [`kagent mcp deploy`](/docs/kagent/1.x/reference/cli/kagent-mcp-deploy/) - Deploy MCP server to Kubernetes -- [`kagent mcp init`](/docs/kagent/1.x/reference/cli/kagent-mcp-init/) - Initialize a new MCP server project -- [`kagent mcp run`](/docs/kagent/1.x/reference/cli/kagent-mcp-run/) - Run MCP server locally -- [`kagent mcp secrets`](/docs/kagent/1.x/reference/cli/kagent-mcp-secrets/) - Manage project secrets +- [`kagent mcp add-tool`]({{< link path="reference/cli/kagent-mcp-add-tool" >}}) - Add a new MCP tool to your project +- [`kagent mcp build`]({{< link path="reference/cli/kagent-mcp-build" >}}) - Build MCP server as a Docker image +- [`kagent mcp deploy`]({{< link path="reference/cli/kagent-mcp-deploy" >}}) - Deploy MCP server to Kubernetes +- [`kagent mcp init`]({{< link path="reference/cli/kagent-mcp-init" >}}) - Initialize a new MCP server project +- [`kagent mcp run`]({{< link path="reference/cli/kagent-mcp-run" >}}) - Run MCP server locally +- [`kagent mcp secrets`]({{< link path="reference/cli/kagent-mcp-secrets" >}}) - Manage project secrets **Flags:** - `-h, --help` - help for mcp diff --git a/docs-site/content/kagent/1.x/reference/tools-ecosystem.md b/docs-site/content/kagent/1.x/reference/tools-ecosystem.md index aabfe976..0118c97c 100644 --- a/docs-site/content/kagent/1.x/reference/tools-ecosystem.md +++ b/docs-site/content/kagent/1.x/reference/tools-ecosystem.md @@ -297,7 +297,7 @@ kagent's repository carries example servers in [contrib/tools](https://github.co To register any other MCP server, write a RemoteMCPServer that points at its URL. [Your first MCP tool]({{< link path="get-started/your-first-mcp-tool#bind-your-own-mcp-server" >}}) works through a complete example. > [!NOTE] -> A RemoteMCPServer takes a URL, so kagent connects only to servers that speak streamable HTTP or Server-Sent Events (SSE). To reach a server that runs over stdio, wrap it in a deployment that exposes an HTTP endpoint. [kmcp](/docs/kmcp/) builds and runs such servers. +> A RemoteMCPServer takes a URL, so kagent connects only to servers that speak streamable HTTP or Server-Sent Events (SSE). To reach a server that runs over stdio, wrap it in a deployment that exposes an HTTP endpoint. [kmcp](https://kagent.dev/docs/kmcp/) builds and runs such servers. ## Next steps diff --git a/docs-site/content/kagent/1.x/setup/model-providers/byo-agentgateway.md b/docs-site/content/kagent/1.x/setup/model-providers/byo-agentgateway.md index 773bbdb4..e90b4592 100644 --- a/docs-site/content/kagent/1.x/setup/model-providers/byo-agentgateway.md +++ b/docs-site/content/kagent/1.x/setup/model-providers/byo-agentgateway.md @@ -12,7 +12,7 @@ author: kagent.dev > [!NOTE] > The `AgentgatewayModel` feature is experimental and disabled by default. Enable it when you install agentgateway by passing `--set agentgatewayModels.enabled=true` to the control plane Helm chart. -1. Install agentgateway in your cluster, adding `--set agentgatewayModels.enabled=true` to the Helm command for the control plane. For more information, see the [agentgateway documentation](https://agentgateway.dev/docs/kubernetes/latest/setup/). +1. Install agentgateway in your cluster, adding `--set agentgatewayModels.enabled=true` to the Helm command for the control plane. For more information, see the [agentgateway documentation](https://agentgateway.dev/docs/kubernetes/latest/documentation/setup/). 2. Create a `Gateway` resource for model routing. ```yaml @@ -39,7 +39,13 @@ author: kagent.dev EOF ``` -3. Create an `AgentgatewayModel` resource for each model that kagent should reach. The resource name becomes the model name that kagent sends in its requests, so it must match the `model` field of the ModelConfig that you create later. The following example routes requests for `gpt-4o-mini` to OpenAI. For more provider and authentication options, see the [agentgateway model documentation](https://docs.solo.io/agentgateway/latest/llm/models/). +3. Store the provider credentials that agentgateway uses to call the model. agentgateway authenticates to the provider on your agents' behalf, so this key belongs to the gateway rather than to kagent. Read the value from the `Authorization` key of the Secret by default. + ```bash + kubectl create secret generic openai-key -n agentgateway-system \ + --from-file=Authorization= + ``` + +4. Create an `AgentgatewayModel` resource for each model that kagent should reach. The resource name becomes the model name that kagent sends in its requests, so it must match the `model` field of the ModelConfig that you create later. The following example routes requests for `gpt-4o-mini` to OpenAI. For more provider and authentication options, see the [agentgateway model documentation](https://docs.solo.io/agentgateway/kubernetes/latest/documentation/llm/models/). ```yaml kubectl apply -f - < [!NOTE] + > `policies.auth` gives the gateway the credentials that it needs to reach the provider. A model with no `auth` is still accepted and programmed, and every request through it returns the provider's own `401`, because agentgateway forwards the call with no credentials. + +5. Save the agentgateway Gateway service address in an environment variable. The `/v1` suffix is required: kagent appends `/chat/completions` to this value, and agentgateway serves that endpoint at `/v1/chat/completions`. Without the suffix, every model call returns `404 Not Found`. ```bash - export AGENTGATEWAY_URL=http://agentgateway-proxy.agentgateway-system.svc.cluster.local + export AGENTGATEWAY_URL=http://agentgateway-proxy.agentgateway-system.svc.cluster.local/v1 ``` ## Create the ModelConfig @@ -89,7 +102,7 @@ EOF | ----- | ----------- | | `model` | The model name to request from agentgateway. This must match the name of an `AgentgatewayModel` resource in your agentgateway deployment. | | `provider` | The provider to use, `OpenAI`, because agentgateway serves an OpenAI-compatible API. | -| `openAI.baseUrl` | The Kubernetes Service address of your agentgateway Gateway. | +| `openAI.baseUrl` | The Kubernetes Service address of your agentgateway Gateway, including the `/v1` path. | {{% /tab %}} {{% tab name="API key authentication" %}} When your agentgateway deployment applies an `apiKeyAuthentication` policy with `mode: Strict`, supply the API key that callers send in the `Authorization` header. @@ -125,7 +138,7 @@ When your agentgateway deployment applies an `apiKeyAuthentication` policy with | `apiKeySecretKey` | The key within that Secret that holds the API key. | | `model` | The model name to request from agentgateway. This must match the name of an `AgentgatewayModel` resource in your agentgateway deployment. | | `provider` | The provider to use, `OpenAI`, because agentgateway serves an OpenAI-compatible API. | - | `openAI.baseUrl` | The Kubernetes Service address of your agentgateway Gateway. | + | `openAI.baseUrl` | The Kubernetes Service address of your agentgateway Gateway, including the `/v1` path. | {{% /tab %}} {{< /tabs >}} diff --git a/docs-site/content/kagent/1.x/setup/model-providers/google-vertexai.md b/docs-site/content/kagent/1.x/setup/model-providers/google-vertexai.md index 6209ffc2..7e245dda 100644 --- a/docs-site/content/kagent/1.x/setup/model-providers/google-vertexai.md +++ b/docs-site/content/kagent/1.x/setup/model-providers/google-vertexai.md @@ -60,7 +60,7 @@ The difference is how each runtime receives the Google credentials. Vertex AI au spec: claude: {} workload: - image: @sha256: + image: {{< reuse "kagent-docs/versions/runtime-image-claude.md" >}} ``` ### What kagent checks before it compiles 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 new file mode 100644 index 00000000..0790aea4 --- /dev/null +++ b/docs-site/content/kagent/1.x/substrate-runtime/networking-and-egress.md @@ -0,0 +1,96 @@ +--- +title: Networking and egress control +description: Learn how Agent Substrate authorizes the outbound connections that your agents open, and how kagent builds the allowlist it enforces. +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. + +> [!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). + +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 + +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. + +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. + +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. + +> [!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 + +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. + +- **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.** + +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. + +## What a rule can match + +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. + +| Rule | Matches | How the gateway dials a match | +| ---- | ------- | ----------------------------- | +| `hostnames` | The request's hostname, against a list of DNS name patterns. | Resolves the name and dials it by name. | +| `cidrs` | The original destination IP, against a list of IPv4 or IPv6 prefixes. | Dials the original address. | +| `all` | Every destination. | Dials the original address. | + +A hostname pattern is a DNS name, optionally with a `*` wildcard replacing the complete leftmost label. The wildcard matches exactly one non-empty label, so `*.example.com` matches `api.example.com` but matches neither `example.com` nor `nested.api.example.com`. No other wildcard syntax is accepted, and a pattern without one matches only the complete name. Patterns must be lowercase, must not carry a trailing dot, and must use the punycode form for an internationalized name. A URL, a bare IP address, and a name with a port are all rejected rather than coerced. + +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 + +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 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. + +> [!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. + +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" >}}). + +## When a 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. + +Because an AgentInstance pins the revision it was created from, changing an AgentTemplate does not move an existing conversation onto a new allowlist. Create a new AgentInstance to pick up a changed set of destinations. + +## Diagnose a denied request + +A denied request fails without explaining itself: the agent sees a rejected call, and the agent's own error text names neither the destination nor the rule. The gateway records the reason instead. + +```bash +kubectl logs -n ate-system deploy/atenet-egress -c agentgateway --tail=100 +``` + +A denial appears as a `403` carrying the Actor that made the request, the host it asked for, and why the request failed. + +```console +error request ... http.host=kagent-tools.kagent http.status=403 + ate.actor.name=ai-01a0abbb-ad73-7dfb-a86e-f2a643dc4dc9 ate.atespace=kagent + 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. + +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. + +> [!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. diff --git a/docs-site/content/kagent/1.x/substrate-runtime/sandboxing.md b/docs-site/content/kagent/1.x/substrate-runtime/sandboxing.md index 507ee412..5d8c1453 100644 --- a/docs-site/content/kagent/1.x/substrate-runtime/sandboxing.md +++ b/docs-site/content/kagent/1.x/substrate-runtime/sandboxing.md @@ -93,4 +93,6 @@ Traffic in the other direction leaves through a separate egress gateway rather t Agent Substrate creates a Kubernetes NetworkPolicy for each WorkerPool, selecting that pool's Worker pods. The policy restricts **ingress** to the Agent Substrate router alone. No other pod in the cluster can open a connection to a Worker, so an Actor is not reachable by anything that bypasses the routing path. -That policy governs inbound traffic only. It does not constrain what an Actor may reach outbound, so outbound access is whatever the surrounding cluster and its infrastructure already allow. Treat network egress as something to configure deliberately for your environment rather than as something the WorkerPool policy settles. +That policy governs inbound traffic only. Outbound traffic is governed separately, by an egress policy that the gateway enforces on every connection that an Actor opens. That policy is default-deny: an Actor reaches a destination only when a rule allows it, and an Actor with no policy at all gets no outbound connection. + +kagent writes that policy for you, deriving it from the AgentTemplate, so an agent needs no egress configuration of its own. For what the policy holds, how a rule is matched, and how to diagnose a denied request, see [Networking and egress control]({{< link path="substrate-runtime/networking-and-egress" >}}). diff --git a/docs-site/go.mod b/docs-site/go.mod index beae2bef..4fb583df 100644 --- a/docs-site/go.mod +++ b/docs-site/go.mod @@ -4,5 +4,5 @@ go 1.21 require ( github.com/imfing/hextra v0.12.3 // indirect - github.com/solo-io/docs-theme-extras v0.3.8 // indirect + github.com/solo-io/docs-theme-extras v0.3.12 // indirect ) diff --git a/docs-site/go.sum b/docs-site/go.sum index 89c6000d..520153e0 100644 --- a/docs-site/go.sum +++ b/docs-site/go.sum @@ -1,4 +1,4 @@ github.com/imfing/hextra v0.12.3 h1:DZHY2rUWYteyzjlHi9r4n7Bb5e2Q+6LXe4C1Dqn0ZjM= github.com/imfing/hextra v0.12.3/go.mod h1:vi+yhpq8YPp/aghvJlNKVnJKcPJ/VyAEcfC1BSV9ARo= -github.com/solo-io/docs-theme-extras v0.3.8 h1:adccTdWRD21FHUhxv49RKW3VbP+y7d+jXzZMxADAWoM= -github.com/solo-io/docs-theme-extras v0.3.8/go.mod h1:jjjYu/QoD+vMu30zgcpfEuTEGuJOJWs5qai/K18kltg= +github.com/solo-io/docs-theme-extras v0.3.12 h1:P9SrPfTLg1AGTQopsXsNj9Dh758iFdzYoHKdmqdFf9U= +github.com/solo-io/docs-theme-extras v0.3.12/go.mod h1:jjjYu/QoD+vMu30zgcpfEuTEGuJOJWs5qai/K18kltg= diff --git a/docs-site/hugo.preview.yaml b/docs-site/hugo.preview.yaml deleted file mode 100644 index c4e3868f..00000000 --- a/docs-site/hugo.preview.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# Preview/authoring overlay. Layered on top of hugo.yaml with -# `--config hugo.yaml,hugo.preview.yaml`, and never used by the production build. -# -# Restores the "1.x" versions entry that hugo.yaml deliberately omits while 1.x is -# withheld. That list does two jobs, and hiding 1.x from the published site breaks -# the second one: -# 1. It drives the version switcher, which is why the entry is absent in production. -# 2. The {{< version >}} shortcode iterates it and emits only on a linkVersion -# match, so with no 1.x entry every {{< version include-if="1.x" >}} renders -# EMPTY -- blanking the conref'd version numbers in assets/kagent-docs/versions/ -# wherever a page reuses them, including the component table on -# reference/versions.md. No error, clean build, broken copy-paste commands. -# -# Hugo REPLACES a list-valued param rather than merging it, so this restates the -# 0.x entry; an overlay holding 1.x alone would drop 0.x from the preview. -# -# Delete this file at the 1.0 release, when the entry returns to hugo.yaml -# alongside dropping the draft keys in content/kagent/1.x/_index.md. -params: - versions: - - version: "0.x" - dropdown: "0.x (current)" - linkVersion: "0.x" - url: "/kagent/0.x" - sections: ["kagent"] - # Restated from hugo.yaml along with the rest of this entry, since Hugo - # replaces a list-valued param rather than merging it. Marking the current - # release is what makes version-noindex.html do anything at all; see the - # long note on the same key in hugo.yaml. - latest: true - - version: "1.x" - dropdown: "1.x (preview)" - linkVersion: "1.x" - url: "/kagent/1.x" - sections: ["kagent"] - banner: "This is preview documentation for the upcoming kagent 1.0 release. It is incomplete and may change without notice." diff --git a/docs-site/hugo.yaml b/docs-site/hugo.yaml index 657f03f8..184f3b36 100644 --- a/docs-site/hugo.yaml +++ b/docs-site/hugo.yaml @@ -129,10 +129,10 @@ params: # partial. A clean build with blank version numbers is the failure mode this # single list prevents. # - # 0.x is the frozen pre-1.0 doc set (moved verbatim from the former flat - # content/kagent/ tree); 1.x is the in-progress v1alpha3/Agent Substrate - # rewrite and carries an in-development banner until it's ready to become - # the default. + # 1.x documents the kagent 1.0 alpha, written against the v1alpha3/Agent + # Substrate API. 0.x is the frozen pre-1.0 set (moved verbatim from the former + # flat content/kagent/ tree) and stays the current supported release while 1.0 + # is in alpha. # # Declaring versions at all moves section resolution off the theme's # version-less positional path (utils/section-segment.html condition (c)) and @@ -144,40 +144,57 @@ params: # nav rooted at site.Home — which is why layouts/_partials/sidebar.html carries # a local override. Verify a kmcp sub-page's sidebar after any change here. versions: + # ORDER sets the order of the dropdown, and is also the theme's fallback + # answer for "the latest version": utils/resolve-latest-version.html takes + # the entry whose `linkVersion` is literally "latest" and, finding none — + # this site versions by 0.x/1.x, not by a /latest/ segment — falls back to + # the FIRST entry. Nothing on this site reads that answer today, because its + # callers are the PDF book cover and the print-book running footer and this + # site enables no `book` output format. 1.x sits first so the dropdown reads + # newest-first. + # + # That partial keys off `linkVersion`, NOT the `latest: true` flag below; + # the two are read by different templates and are not interchangeable. + - version: "1.x" + dropdown: "1.x (alpha)" + linkVersion: "1.x" + url: "/kagent/1.x" + sections: ["kagent"] + # kagent 1.0 ships as an alpha, so this entry deliberately does not carry + # `latest`. The flag stays on the 0.x entry below; move it here at 1.0 GA. + # + # Alpha banner. Rendered by the local layouts/partials/version-banner.html + # override, which matches an entry by exact path segment and runs the text + # through `markdownify` — so the link below works, and a kmcp page matches + # nothing and shows no banner. Delete this line at GA. + banner: "This documentation covers the kagent 1.0 alpha. For the latest 0.x release, see the [0.x docs](/docs/kagent/0.x/)." - version: "0.x" dropdown: "0.x (current)" linkVersion: "0.x" url: "/kagent/0.x" sections: ["kagent"] - # `latest` marks the current release, and it is one of only two signals - # docs-theme-extras' utils/version-noindex.html accepts (the other is a - # `linkVersion: "latest"` URL segment, which this site does not use). - # Without it the partial matches nothing and emits no robots meta at all, - # so no version is ever marked as superseding another. 0.x is correct - # today: kagent v0.10.1 is the released chart. + # `latest` marks the current release, and v0.10.1 is still that release + # while 1.0 is in alpha. It is one of only two signals + # utils/version-noindex.html accepts, and the only one available to this + # site (the other is a `linkVersion: "latest"` URL segment, which this + # site does not use). Without it that partial matches nothing and emits no + # robots meta at all, so no version is ever marked as superseding another. # - # It changes nothing in the current production build, because 1.x is - # draft and no second version is rendered. It starts mattering the moment - # 1.x publishes: the partial noindexes a NON-latest page whenever the same - # page exists in the latest version, so with `latest` left here, 1.x pages - # that duplicate a 0.x page would be noindexed — the theme's deliberate - # "unreleased docs should not outrank the current release" behavior. - # Whether 1.0 takes `latest` over at release is an open decision; see - # RELEASE-1.0-CHECKLIST.md. + # EXACTLY ONE entry may carry it. On both, or on neither, the partial + # silently does nothing. + # + # It starts mattering now that 1.x publishes. The partial noindexes a + # NON-latest page whenever the same page exists in the latest version, so + # the 1.x pages whose path also exists under 0.x get `noindex, follow` — + # the 1.x landing page among them, since /kagent/0.x is its equivalent. + # Every other 1.x page has no 0.x counterpart and stays indexable. That is + # the theme's deliberate "unreleased docs should not outrank the current + # release" behavior. Moving the flag to the 1.x entry at GA reverses the + # polarity, and 0.x pages get noindexed instead. latest: true - # The "1.x" entry is deliberately absent until the 1.0 release is announced. - # This list drives the version switcher independently of whether the pages - # exist, so leaving the entry here while the content is drafted publishes a - # "1.x (preview)" menu item pointing at a 404. hugo.preview.yaml layers it - # back for local authoring; restore it here alongside dropping the draft keys - # in content/kagent/1.x/_index.md: - # - # - version: "1.x" - # dropdown: "1.x (preview)" - # linkVersion: "1.x" - # url: "/kagent/1.x" - # sections: ["kagent"] - # banner: "This is preview documentation for the upcoming kagent 1.0 release. It is incomplete and may change without notice." + # Alpha announcement banner, rendered by the same override as the 1.x + # banner above. Replace it with a superseded-version notice at GA. + banner: "kagent 1.0 is available as an alpha release. To read its documentation, see the [1.x docs](/docs/kagent/1.x/)." # Label on the section selector button. Without it the button falls back to # site.Title ("kagent docs"), which reads oddly above a menu offering kagent diff --git a/docs-site/layouts/_shortcodes/gloss.html b/docs-site/layouts/_shortcodes/gloss.html index 652029c8..8831356d 100644 --- a/docs-site/layouts/_shortcodes/gloss.html +++ b/docs-site/layouts/_shortcodes/gloss.html @@ -1,10 +1,12 @@ -{{- /* kagent override of docs-theme-extras' gloss shortcode. +{{- /* kagent override of docs-theme-extras' gloss shortcode. Re-synced against + upstream v0.3.12. Why this exists: upstream hardcodes target="_blank" on the optional - "Learn more" link, which is right for an external reference and wrong - for a link to another page in this docset — it strands the reader in a - new tab for an ordinary cross-reference. That single attribute was the - reason data/glossary.yaml carried no `link` values at all. + "Learn more" link, which suits an external reference and misfires on a + link to another page in this docset — it strands the reader in a new tab + for an ordinary cross-reference. Every `link` value in data/glossary.yaml + is an internal /docs/kagent/… path, so upstream's markup would open a new + tab for all of them. Fix: open a new tab only for an absolute http(s) destination, and add rel="noopener" while doing so. An internal, root-relative link opens in @@ -16,7 +18,30 @@ never reaches this template — the override must carry the rename itself or break when the removal lands. Re-sync when extras converts. - Everything else is byte-for-byte identical to upstream v0.2.0. */ -}} + Those two lines are the whole delta. Everything else matches upstream + v0.3.12. */ -}} +{{- /* KEEP THE TERM MARKUP BELOW ON ONE LINE, with no whitespace between its + tags. This is not a formatting preference, and the reason is carried from + upstream, where reformatting breaks two things in the same edit, silently: + + - `utils/md-strip-glossary.html` matches + `` and `` as + LITERAL adjacencies. Indented markup stops being stripped, and the + whole definition reappears inline in every `.md` URL and + Copy-as-Markdown payload — the bug that partial exists to fix. + page-to-markdown.html runs that strip on every page, and kagent emits + `markdown` for both `page` and `section` (hugo.yaml `outputs`), so + this build depends on it. + - upstream's `glossary-tooltip-inlined` scan in copy-md-fidelity.spec.ts + keys on the bolded key ABUTTING the display text, and that scan is + what guards the strip. It runs against upstream's own fixture, never + against this file, so on the kagent side the leak has no test behind + it — the markup itself is the only safeguard. + + The conditional target/rel added below sits inside the optional ``, + which the strip regex absorbs as `]*>`, so the override's extra + attributes are safe. The adjacencies ahead of it are the part to leave + alone. */ -}} {{- $key := .Get 0 -}} {{- $entry := index hugo.Data.glossary $key -}} {{- if $entry -}} diff --git a/docs-site/layouts/partials/utils/version-noindex.html b/docs-site/layouts/partials/utils/version-noindex.html new file mode 100644 index 00000000..1580ca94 --- /dev/null +++ b/docs-site/layouts/partials/utils/version-noindex.html @@ -0,0 +1,64 @@ +{{- /* kagent override of docs-theme-extras' utils/version-noindex.html. + + Why this exists: the upstream partial builds the current-version + equivalent of the page from `utils/version-root.html`'s `lookupPath` and + passes it straight to `site.GetPage`. That works on the two site shapes + upstream handles, and silently fails on this one. + + kagent's baseURL is https://kagent.dev/docs/, so every RelPermalink + starts with a "/docs/" segment that comes from the baseURL PATH, not from + a content directory — content/kagent/1.x/... is served at + /docs/kagent/1.x/.... `site.GetPage` resolves paths relative to + contentDir, so it never matches a path carrying that prefix: + + site.GetPage "/docs/kagent/1.x/observability/tracing" -> nil + site.GetPage "/kagent/1.x/observability/tracing" -> the page + + Upstream's `lookupPath` comment calls the prefix "genuinely part of the + path" on an OSS site, which holds where contentDir is the repo root. It + does not hold here, so every lookup missed, no duplicate was ever found, + and the partial emitted nothing on any page. That was invisible while 1.x + was withheld by a draft cascade: one rendered version gave the partial + nothing to mark either way. It stops being invisible now that 1.x + publishes, which is when this override was added. + + Fix: strip the site's own home prefix before the GetPage lookup, and + nothing else. Every other line matches upstream, including the + duplicates-only behavior — a non-latest page with no equivalent in the + latest version is left indexable on purpose. `latest` sits on the 0.x + entry while 1.0 is in beta, so today that spares every 1.x page the 0.x + tree has no counterpart for. + + Re-check this against the pinned module on any docs-theme-extras bump: if + upstream learns to resolve a baseURL path prefix itself, this override + should be deleted rather than left to rot. */ -}} + +{{- $root := partial "utils/version-root.html" . -}} +{{- with partial "utils/resolve-section-versions.html" $root.section -}} + {{- /* The current version's URL segment. `latest = true` is the only signal + available on this site; the upstream fallback is a `linkVersion` of + literally "latest", which kagent does not use. Exactly one entry may + carry the flag — on both, or on neither, this partial does nothing. */ -}} + {{- $latestSeg := "" -}} + {{- range . -}}{{ if eq (.latest | default false) true }}{{ $latestSeg = .linkVersion | default .version }}{{ end }}{{- end -}} + {{- if not $latestSeg -}} + {{- range . -}}{{ if eq (.linkVersion | default "") "latest" }}{{ $latestSeg = "latest" }}{{ end }}{{- end -}} + {{- end -}} + + {{- if and $latestSeg $root.isVersioned (ne $root.currentVersion $latestSeg) -}} + {{- $lookupPrefix := strings.TrimSuffix (printf "%s/" $root.currentVersion) $root.lookupPath -}} + {{- $latestEquiv := printf "%s%s/%s" $lookupPrefix $latestSeg $root.pathAfterVersion -}} + {{- $latestEquiv = strings.TrimRight "/" $latestEquiv -}} + + {{- /* THE OVERRIDE. site.Home.RelPermalink is "/docs/" here and "/" on a + site served from the root, where this is a no-op. */ -}} + {{- $homePrefix := strings.TrimSuffix "/" site.Home.RelPermalink -}} + {{- if and $homePrefix (hasPrefix $latestEquiv (printf "%s/" $homePrefix)) -}} + {{- $latestEquiv = strings.TrimPrefix $homePrefix $latestEquiv -}} + {{- end -}} + + {{- if site.GetPage $latestEquiv -}} + + {{- end -}} + {{- end -}} +{{- end -}} diff --git a/docs-site/playwright/README.md b/docs-site/playwright/README.md index ba4e5029..2ae17fa2 100644 --- a/docs-site/playwright/README.md +++ b/docs-site/playwright/README.md @@ -67,7 +67,19 @@ Two constraints on the mock path: (`ui/package.json`), and its build tooling fails on Node 18 with a `styleText` import error from `node:util` that does not name the cause. - For live-cluster captures: `kind`, `helm`, `kubectl`, `kubectl-ate`, `jq`, `openssl`, and a - model provider API key. + model provider API key. The provisioner checks for all of these before it touches the cluster + and names whichever is missing, so you find out up front rather than half way through an + install. +- **`kubectl-ate` is not on most machines and must match the Agent Substrate version** the + capture installs, because it writes that version's identity material. It is published per + release rather than through a package manager: + ```sh + curl -sSL -o kubectl-ate \ + "https://github.com/kagent-dev/substrate/releases/download/v/kubectl-ate-$(uname -s | tr '[:upper:]' '[:lower:]')-amd64" + chmod +x kubectl-ate && mv kubectl-ate /usr/local/bin/ # or anywhere on PATH + ``` + Read `` from `assets/kagent-docs/versions/agent-substrate.md`, which is the + same conref the provisioner reads. - One-time setup: ```sh cd docs-site/playwright diff --git a/docs-site/playwright/provisioners/kagent-kind.sh b/docs-site/playwright/provisioners/kagent-kind.sh index 4fb75bd4..f8b797c2 100755 --- a/docs-site/playwright/provisioners/kagent-kind.sh +++ b/docs-site/playwright/provisioners/kagent-kind.sh @@ -140,6 +140,24 @@ if $DELETE; then exit 0 fi +# Fail on a missing tool up front, naming it. Without this the first absent tool +# surfaces mid-run as its own unhelpful error -- a missing kubectl-ate reads as +# `unknown command "ate" for "kubectl"` -- and by then Agent Substrate is installed, +# so the cluster is half-built. kubectl-ate must match SUBSTRATE_VERSION; it is +# published at https://github.com/kagent-dev/substrate/releases. +if ! $DRY_RUN; then + missing=() + for tool in kind helm kubectl jq openssl; do + command -v "$tool" >/dev/null 2>&1 || missing+=("$tool") + done + kubectl ate --help >/dev/null 2>&1 || missing+=("kubectl-ate") + if (( ${#missing[@]} )); then + echo "Missing required tool(s): ${missing[*]}" >&2 + echo "See the Prerequisites section of docs-site/playwright/README.md." >&2 + exit 1 + fi +fi + if ! $DRY_RUN && [[ -z "${OPENAI_API_KEY:-}" ]]; then echo "OPENAI_API_KEY is required (the kagent chart needs a model provider key)." >&2 echo "Re-run with --dry-run to see the commands without installing." >&2 @@ -170,9 +188,19 @@ echo # Skip creation when the cluster is already there, so a re-run after a failed install # picks up where it left off instead of dying on `kind create`. Every step below is a # `helm upgrade --install` or a `kubectl create` for the same reason. +# +# Reuse it only when it also ANSWERS. `kind get clusters` lists a cluster whose node +# container is stopped -- which is what Docker Desktop leaves behind when it restarts -- +# and installing into that one sends every step at an unreachable API server, failing +# with `Kubernetes cluster unreachable` once per step rather than once up front. if ! $NO_CLUSTER && ! $DRY_RUN && kind get clusters 2>/dev/null | grep -qx "$CLUSTER_NAME"; then - echo "kind cluster '${CLUSTER_NAME}' already exists; skipping create." - NO_CLUSTER=true + if kubectl --context "kind-${CLUSTER_NAME}" cluster-info >/dev/null 2>&1; then + echo "kind cluster '${CLUSTER_NAME}' already exists and is reachable; skipping create." + NO_CLUSTER=true + else + echo "kind cluster '${CLUSTER_NAME}' exists but does not answer; recreating it." >&2 + run kind delete cluster --name "$CLUSTER_NAME" + fi fi if ! $NO_CLUSTER; then @@ -236,11 +264,18 @@ kubectl create secret generic actor-id-ca-certs -n ${ATE_NAMESPACE} \ " run_sh "kubectl create configmap ate-api-authentication" " +set -euo pipefail +# Ask the cluster for its issuer rather than assuming one. kind 1.37 advertises +# https://kubernetes.default.svc.cluster.local, while the older hardcoded value was +# https://kubernetes.default.svc; a mismatch is accepted at install time and only shows +# up later as \`token issuer ... not trusted\` on every kubectl-ate and ateapi call. +# kagent's own setup-cluster.sh derives it the same way (kagent#2763, fixed in #2770). +k8s_issuer=\"\$(kubectl get --raw /.well-known/openid-configuration | jq -r .issuer)\" kubectl create configmap ate-api-authentication -n ${ATE_NAMESPACE} \ --from-literal=authentication.yaml='actorIdentityJWTProvider: kubernetes jwtProviders: - name: kubernetes - issuer: https://kubernetes.default.svc + issuer: '\"\${k8s_issuer}\"' audiences: [api.${ATE_NAMESPACE}.svc] certificateAuthorityFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt discoveryTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token diff --git a/scripts/generate-cli-docs.py b/scripts/generate-cli-docs.py index 1bdc295c..fbb78907 100755 --- a/scripts/generate-cli-docs.py +++ b/scripts/generate-cli-docs.py @@ -311,7 +311,24 @@ def render_flags(flags: list[tuple[str, str]]) -> str: return "\n".join(lines) -def render_page(display_name: str, node: CommandNode, weight: int, description: str, url_prefix: str) -> str: +def cli_link(url_prefix: str, link_prefix: str | None, slug: str) -> str: + """Return the link destination for one generated CLI page. + + With `link_prefix` set, emit the theme's version- and product-aware `link` + shortcode. A hardcoded root-relative URL bakes in the OSS version segment + (/docs/kagent/1.x/...), which is correct upstream and wrong anywhere the + page is reused: the enterprise docs hub rebases these pages under its own + version, and the shortcode is what lets one source resolve in both. + + Without it, fall back to the root-relative URL. kmcp has no version tree, + so it has nothing for the shortcode to resolve against and keeps the URL. + """ + if link_prefix: + return '{{< link path="%s/%s" >}}' % (link_prefix, slug) + return f"{url_prefix}/{slug}/" + + +def render_page(display_name: str, node: CommandNode, weight: int, description: str, url_prefix: str, link_prefix: str | None) -> str: full_use = " ".join([display_name, *node.path]) title = full_use @@ -327,13 +344,13 @@ def render_page(display_name: str, node: CommandNode, weight: int, description: body_parts.append("**Subcommands:**") for child in node.children: child_full = " ".join([display_name, *node.path, child]) - # Root-relative link, not "../"-prefixed. The site publishes - # both an HTML version of every page (served at a pretty-URL - # directory, where "../" resolves correctly) and a raw .md - # version (served at a flat file URL, where "../" resolves one - # level too far up and 404s). A root-relative link resolves - # identically in both. - body_parts.append(f"- [`{child_full}`]({url_prefix}/{slugify(display_name, node.path + [child])}/) - {node.child_short[child]}") + # Never a "../"-prefixed link. The site publishes both an HTML + # version of every page (served at a pretty-URL directory, where + # "../" resolves correctly) and a raw .md version (served at a flat + # file URL, where "../" resolves one level too far up and 404s). + # Both forms cli_link can return resolve identically in both. + dest = cli_link(url_prefix, link_prefix, slugify(display_name, node.path + [child])) + body_parts.append(f"- [`{child_full}`]({dest}) - {node.child_short[child]}") body_parts.append("") if node.flags: @@ -384,6 +401,17 @@ def main() -> int: "correctly for one of those two forms." ), ) + parser.add_argument( + "--link-prefix", + default=None, + help=( + "Section path relative to the version root, e.g. reference/cli. " + "When set, subcommand and _index.md links are emitted as the " + "version-aware `link` shortcode instead of a root-relative URL, so " + "they resolve correctly both here and wherever the page is reused " + "downstream. Omit for an unversioned section such as kmcp." + ), + ) parser.add_argument( "--weight", type=int, @@ -451,7 +479,7 @@ def write_subtree(path: list[str], weight_start: int) -> int: # description across several lines in --help output. parent = nodes[tuple(path[:-1])] description = parent.child_short[path[-1]] - page = render_page(args.display_name, node, w, description, url_prefix) + page = render_page(args.display_name, node, w, description, url_prefix, args.link_prefix) filename = f"{slugify(args.display_name, path)}.md" (out_dir / filename).write_text(page) w += 10 @@ -463,7 +491,8 @@ def write_subtree(path: list[str], weight_start: int) -> int: # Root-relative, same reasoning as the Subcommands links in # render_page: resolves correctly for both the HTML (pretty-URL # directory) and the parallel .md export (flat file) of this page. - index_lines.append(f"- [`{args.display_name} {child}`]({url_prefix}/{slugify(args.display_name, [child])}/) - {root.child_short[child]}") + dest = cli_link(url_prefix, args.link_prefix, slugify(args.display_name, [child])) + index_lines.append(f"- [`{args.display_name} {child}`]({dest}) - {root.child_short[child]}") weight = write_subtree([child], weight) (out_dir / "_index.md").write_text("\n".join(index_lines) + "\n")