From 356d5a8d60f7b7416931d738d73bbab2b9fbba9d Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 04:12:13 -0400 Subject: [PATCH 01/12] feat(helm): add an example Agent with the kagent-tools skills Signed-off-by: Danielle Mustillo --- .gitignore | 1 + Makefile | 14 +- .../configuration-and-compilation.md | 9 + docs/env.md | 1 + go/core/pkg/env/testing.go | 1 + go/core/test/e2e/README.md | 8 + go/core/test/e2e/example_agent_skills_test.go | 100 +++++++++ go/core/test/e2e/interaction_test.go | 2 +- .../mocks/invoke_example_agent_skills.json | 203 ++++++++++++++++++ helm/README.md | 67 ++++++ helm/kagent/templates/_helpers.tpl | 20 ++ helm/kagent/templates/example-agent.yaml | 150 +++++++++++++ helm/kagent/tests/example-agent_test.yaml | 191 ++++++++++++++++ helm/kagent/values.yaml | 26 ++- scripts/package-release-charts.sh | 25 +++ scripts/package-release-charts_test.sh | 115 +++++++++- scripts/tools-image-metadata.sh | 32 +++ 17 files changed, 960 insertions(+), 5 deletions(-) create mode 100644 go/core/test/e2e/example_agent_skills_test.go create mode 100644 go/core/test/e2e/mocks/invoke_example_agent_skills.json create mode 100644 helm/kagent/templates/example-agent.yaml create mode 100644 helm/kagent/tests/example-agent_test.yaml create mode 100644 scripts/tools-image-metadata.sh diff --git a/.gitignore b/.gitignore index 849b205b75..957413efde 100644 --- a/.gitignore +++ b/.gitignore @@ -209,6 +209,7 @@ file::memory:* ## Helm /helm/kagent/files/builtin-harness-images.json +/helm/kagent/files/kagent-tools-image.json *.tgz /tmp/ /reports/*.csv diff --git a/Makefile b/Makefile index a3b680d841..c8254c32a6 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ HELM_REPO ?= oci://ghcr.io/kagent-dev HELM_DIST_FOLDER ?= dist HELM_SOURCE_DIR ?= helm RUNTIME_IMAGE_REPOSITORY ?= ghcr.io/kagent-dev/kagent +# The kagent-tools image whose skills the chart's example Agent mounts. Its tag +# is the kagent-tools chart version that helm/kagent depends on. +TOOLS_IMAGE_REPOSITORY ?= ghcr.io/kagent-dev/kagent/tools # Release packaging requires an explicit metadata directory; local builds have a default. RUNTIME_BUILD_METADATA_DIR = $(or $(RUNTIME_IMAGE_METADATA_DIR),$(CURDIR)/build/runtime-images) RUNTIME_IMAGE_METADATA_FILE = $(RUNTIME_BUILD_METADATA_DIR)/$(patsubst build-%,%,$@).json @@ -619,9 +622,18 @@ helm-release: ## Package release charts from RUNTIME_IMAGE_METADATA_DIR without @test -n "$(RUNTIME_IMAGE_METADATA_DIR)" || { echo "RUNTIME_IMAGE_METADATA_DIR is required for release chart packaging" >&2; exit 1; } bash scripts/package-release-charts.sh "$(VERSION)" "$(RUNTIME_IMAGE_METADATA_DIR)" "$(HELM_SOURCE_DIR)" "$(HELM_DIST_FOLDER)" "$(RUNTIME_IMAGE_REPOSITORY)" +.PHONY: tools-image-metadata +tools-image-metadata: ## Record the digest of the kagent-tools image the chart pins in RUNTIME_IMAGE_METADATA_DIR + @test -n "$(RUNTIME_IMAGE_METADATA_DIR)" || { echo "RUNTIME_IMAGE_METADATA_DIR is required for release chart packaging" >&2; exit 1; } + mkdir -p "$(RUNTIME_IMAGE_METADATA_DIR)" + rm -f "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json" "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" + bash scripts/tools-image-metadata.sh "$(HELM_SOURCE_DIR)/kagent/Chart-template.yaml" "$(TOOLS_IMAGE_REPOSITORY)" > "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" || { rm -f "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp"; exit 1; } + if [ -s "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" ]; then mv "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json"; else rm -f "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp"; fi + .PHONY: helm-publish helm-publish: ## Package and push all Helm charts to the OCI registry -helm-publish: helm-release +helm-publish: tools-image-metadata + $(MAKE) --no-print-directory helm-release helm push "$(HELM_DIST_FOLDER)/kagent-crds-$(VERSION).tgz" $(HELM_REPO)/kagent/helm helm push "$(HELM_DIST_FOLDER)/kagent-$(VERSION).tgz" $(HELM_REPO)/kagent/helm diff --git a/docs/architecture/configuration-and-compilation.md b/docs/architecture/configuration-and-compilation.md index 1ea1a26da0..704eb594c5 100644 --- a/docs/architecture/configuration-and-compilation.md +++ b/docs/architecture/configuration-and-compilation.md @@ -95,6 +95,15 @@ Local installations can override individual defaults with `config.harnessImages`; `global.imageRegistry` rewrites both packaged defaults and explicit overrides. +The chart's optional example Agent mounts skills from the kagent-tools image. +That image is released from its own repository, so no build here records its +digest. Before packaging, `make helm-publish` runs `make tools-image-metadata`, +which resolves the digest of the kagent-tools version in the chart's +dependencies from the registry and writes it to `RUNTIME_IMAGE_METADATA_DIR`. +`make helm-release` validates that metadata when present and packages +`files/kagent-tools-image.json`. Without it, the chart renders the example +Agent only with an explicit `exampleAgent.skillsImage`. + The outer compiler sets the selected image on the compiled revision before hashing it. Adapters receive the requested Harness spec; provenance records that spec without injected defaults. A missing or invalid diff --git a/docs/env.md b/docs/env.md index 00e4fe226a..3ae2dae599 100644 --- a/docs/env.md +++ b/docs/env.md @@ -180,6 +180,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_E2E_RUN_UPGRADE_TESTS` | String | `false` | Run upgrade integration tests when exactly true. | | `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | +| `KAGENT_E2E_TOOLS_SKILLS_IMAGE` | String | `(none)` | Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agent E2E test. That test is skipped when unset. | | `KAGENT_E2E_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50. | | `KAGENT_E2E_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | | `KAGENT_E2E_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index 13c4473d18..e248c97e16 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -56,6 +56,7 @@ var ( _ = RegisterStringVar("KAGENT_E2E_BYO_IMAGE", "", "Digest-pinned BYO image substituted into E2E lifecycle manifests.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_CLAUDE_IMAGE", "", "Digest-pinned Claude harness image substituted into E2E lifecycle manifests.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_CODEX_IMAGE", "", "Digest-pinned Codex harness image substituted into E2E lifecycle manifests.", ComponentTesting) + E2EToolsSkillsImage = RegisterStringVar("KAGENT_E2E_TOOLS_SKILLS_IMAGE", "", "Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agent E2E test. That test is skipped when unset.", ComponentTesting) E2ESandboxNamespace = RegisterStringVar("KAGENT_E2E_SANDBOX_NAMESPACE", "kagent", "Namespace for sandbox E2E resources.", ComponentTesting) E2ESandboxWorkerPool = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) E2EOTLPListenAddress = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) diff --git a/go/core/test/e2e/README.md b/go/core/test/e2e/README.md index 5bce3630a0..b05122115a 100644 --- a/go/core/test/e2e/README.md +++ b/go/core/test/e2e/README.md @@ -7,6 +7,14 @@ the three `config.harnessImages` defaults matching the explicit Harness fixtures. The test checks runtime readiness, effective-image status, and that the controller leaves the user spec unchanged. +`TestExampleAgentToolsSkills` runs the chart's example Agent behavior on every +harness with a mock model. It needs `KAGENT_E2E_TOOLS_SKILLS_IMAGE`, a +digest-pinned kagent-tools image that contains the `opt/kagent-tools-skills` +Agent Plugin, in the local test registry; the test is skipped without it. It +checks that the seven skills are advertised and that `helm-release-management` +loads. It omits the tool server binding because the test install does not run +kagent-tools. + The standalone sandbox suite needs a Substrate WorkerPool with available capacity. Set `config.sandboxes.guestImage` to the guest image built for the test. The controller passes the pinned image reference diff --git a/go/core/test/e2e/example_agent_skills_test.go b/go/core/test/e2e/example_agent_skills_test.go new file mode 100644 index 0000000000..b8bd46f4c1 --- /dev/null +++ b/go/core/test/e2e/example_agent_skills_test.go @@ -0,0 +1,100 @@ +package e2e_test + +import ( + "encoding/json" + "slices" + "strings" + "testing" + + a2atype "github.com/a2aproject/a2a-go/v2/a2a" + "github.com/kagent-dev/mockllm" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kagent-dev/kagent/go/api/v1alpha3" + kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" +) + +// exampleAgentSkills are the kagent-tools skills that the chart's example Agent +// (helm/kagent/templates/example-agent.yaml) selects. +var exampleAgentSkills = []string{ + "k8s-troubleshooting", + "helm-release-management", + "istio-traffic-debugging", + "argo-rollouts-progressive-delivery", + "cilium-network-debugging", + "kubescape-security-posture", + "prometheus-observability", +} + +// TestExampleAgentToolsSkills runs the chart's example Agent behavior on every +// harness with a mock model: the kagent-tools image as an Agent Plugin, its +// seven skills, and the built-in prompt library. The chart's tool server +// binding is left out because the E2E install does not run kagent-tools. +func TestExampleAgentToolsSkills(t *testing.T) { + t.Parallel() + target := interactionTarget(t) + image := kagentenv.E2EToolsSkillsImage.Get() + if image == "" { + t.Skip("KAGENT_E2E_TOOLS_SKILLS_IMAGE is not set; it needs a kagent-tools image with the opt/kagent-tools-skills folder") + } + forEachHarness(t, func(t *testing.T, harness testHarness) { + t.Parallel() + model := startExampleAgentSkillsMock(t, harness.skillsRoot()) + kube := interactionKubeClient(t) + config := harness.createModel(t, kube, reachableModelURL(t, model.URL), nil) + template := &v1alpha3.AgentTemplate{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "example-skills-", Namespace: "kagent", Labels: harness.labels()}, + Spec: v1alpha3.AgentTemplateSpec{ + ModelConfig: &corev1.LocalObjectReference{Name: config.Name}, + Description: "Kubernetes operations assistant that uses the kagent-tools skills and MCP tools.", + PromptTemplate: &v1alpha3.AgentTemplatePromptTemplateSpec{ + DataSources: []v1alpha3.AgentTemplatePromptSource{{Name: "kagent-builtin-prompts", Alias: "builtin"}}, + }, + SystemPrompt: "Answer questions about the Kubernetes cluster where kagent runs. When a\n" + + "question matches one of your skills, load that skill and follow it. Each\n" + + "skill names the kagent-tools MCP tools it uses.\n\n" + + "{{include \"builtin/skills-usage\"}}\n\n" + + "{{include \"builtin/kubernetes-context\"}}\n\n" + + "{{include \"builtin/tool-usage-best-practices\"}}\n\n" + + "{{include \"builtin/safety-guardrails\"}}\n", + Plugins: []v1alpha3.PluginBundle{{ + Source: v1alpha3.ArtifactSource{OCI: image, Path: "opt/kagent-tools-skills"}, + Skills: exampleAgentSkills, + }}, + }, + } + // Ready requires the runtime to mount the image and install the skills. + createAndWaitInteractionTemplate(t, harness, kube, template) + fixture := newInteractionFixtureForTemplate(t, harness, target, template.Name) + _, _, task := fixture.send(t, "Which Helm releases are installed?") + require.Equal(t, a2atype.TaskStateCompleted, task.Status.State, taskText(task)) + require.Contains(t, taskText(task), "EXAMPLE_SKILL_LOADED") + + // Claude also sends requests without a body; only model turns count. + requests := slices.DeleteFunc(model.Requests("", ""), func(r recordedModelRequest) bool { return len(r.Body) == 0 }) + require.GreaterOrEqual(t, len(requests), 2, "the model must be called again after loading the skill") + first := string(requests[0].Body) + for _, skill := range exampleAgentSkills { + require.Contains(t, first, skill, "skill must be advertised to the model") + } + require.Contains(t, first, "## Safety Guidelines", "built-in prompts must be included") + // Discovery exposes metadata only. The skill body names its MCP tools. + require.NotContains(t, first, "helm_list_releases") + loaded := false + for _, request := range requests[1:] { + loaded = loaded || strings.Contains(string(request.Body), "helm_list_releases") + } + require.True(t, loaded, "the model did not receive the helm-release-management skill body") + }) +} + +func startExampleAgentSkillsMock(t *testing.T, root string) *modelRecorder { + t.Helper() + fixture, err := interactionMocks.ReadFile("mocks/invoke_example_agent_skills.json") + require.NoError(t, err) + var config mockllm.Config + require.NoError(t, json.Unmarshal([]byte(strings.ReplaceAll(string(fixture), "ROOT", root)), &config)) + return startModelRecorder(t, mockLLMProxy(t, startMockLLMConfig(t, config)), nil) +} diff --git a/go/core/test/e2e/interaction_test.go b/go/core/test/e2e/interaction_test.go index 04a1cf8163..0bd1e8ee1f 100644 --- a/go/core/test/e2e/interaction_test.go +++ b/go/core/test/e2e/interaction_test.go @@ -50,7 +50,7 @@ import ( ) //go:embed mocks/invoke_agent.json mocks/invoke_golang_hitl_ask_user.json mocks/invoke_mcp_agent.json mocks/invoke_shared_agent.json mocks/invoke_structured_output.json mocks/invoke_native_ask_user.json -//go:embed mocks/invoke_mounted_skills.json mocks/invoke_egress_skill.json +//go:embed mocks/invoke_mounted_skills.json mocks/invoke_egress_skill.json mocks/invoke_example_agent_skills.json var interactionMocks embed.FS const structuredOutputSchema = `{"type":"object","properties":{"answer":{"type":"integer"},"explanation":{"type":"string"}},"required":["answer","explanation"],"additionalProperties":false}` diff --git a/go/core/test/e2e/mocks/invoke_example_agent_skills.json b/go/core/test/e2e/mocks/invoke_example_agent_skills.json new file mode 100644 index 0000000000..5cc030eb92 --- /dev/null +++ b/go/core/test/e2e/mocks/invoke_example_agent_skills.json @@ -0,0 +1,203 @@ +{ + "openai": [ + { + "name": "example_skills_load", + "match": { + "match_type": "contains", + "message": { + "role": "user", + "content": "Which Helm releases are installed?" + } + }, + "response": { + "id": "example_skills_load", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4.1-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_load_helm_release_management", + "type": "function", + "function": { + "name": "load_skill", + "arguments": "{\"name\": \"helm-release-management\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ] + } + }, + { + "name": "example_skills_loaded", + "match": { + "match_type": "contains", + "message": { + "role": "tool", + "content": "helm_list_releases" + } + }, + "response": { + "id": "example_skills_loaded", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4.1-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "EXAMPLE_SKILL_LOADED" + }, + "finish_reason": "stop" + } + ] + } + } + ], + "openai_response": [ + { + "name": "example_skills_load", + "match": { + "match_type": "contains", + "input": "Which Helm releases are installed?" + }, + "response": { + "id": "example_skills_load", + "object": "response", + "created_at": 1677652290, + "model": "gpt-5.2-codex", + "output": [ + { + "id": "fc_load_helm_release_management", + "type": "function_call", + "status": "completed", + "call_id": "call_load_helm_release_management", + "name": "exec_command", + "arguments": "{\"cmd\": \"cat ROOT/helm-release-management/SKILL.md\"}" + } + ], + "parallel_tool_calls": false, + "tools": [], + "usage": { + "input_tokens": 10, + "output_tokens": 8, + "total_tokens": 18 + } + } + }, + { + "name": "example_skills_loaded", + "match": { + "match_type": "contains", + "input": "helm_list_releases" + }, + "response": { + "id": "example_skills_loaded", + "object": "response", + "created_at": 1677652291, + "model": "gpt-5.2-codex", + "output": [ + { + "id": "msg_example_skills_loaded", + "type": "message", + "status": "completed", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "EXAMPLE_SKILL_LOADED", + "annotations": [] + } + ] + } + ], + "parallel_tool_calls": false, + "tools": [], + "usage": { + "input_tokens": 14, + "output_tokens": 5, + "total_tokens": 19 + } + } + } + ], + "anthropic": [ + { + "name": "example_skills_load", + "match": { + "match_type": "contains", + "message": { + "role": "user", + "content": [ + { + "type": "text", + "text": "Which Helm releases are installed?" + } + ] + } + }, + "response": { + "id": "example_skills_load", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_load_helm_release_management", + "name": "Skill", + "input": { + "skill": "helm-release-management" + } + } + ], + "model": "claude-sonnet-4-5", + "stop_reason": "tool_use", + "usage": { + "input_tokens": 10, + "output_tokens": 4 + } + } + }, + { + "name": "example_skills_loaded", + "match": { + "match_type": "contains", + "message": { + "role": "user", + "content": [ + { + "type": "text", + "text": "helm_list_releases" + } + ] + } + }, + "response": { + "id": "example_skills_loaded", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "EXAMPLE_SKILL_LOADED" + } + ], + "model": "claude-sonnet-4-5", + "stop_reason": "end_turn", + "usage": { + "input_tokens": 14, + "output_tokens": 4 + } + } + } + ] +} diff --git a/helm/README.md b/helm/README.md index 30030d1b96..073f844b77 100644 --- a/helm/README.md +++ b/helm/README.md @@ -231,6 +231,73 @@ release namespace. **Note**: MicroVM requires a `microvm` SandboxConfig, runtime assets, and KVM-capable workers. kagent does not install these prerequisites. +#### Example Agent + +Set `exampleAgent.enabled: true` and `exampleAgent.snapshotLocation` to install +one example Agent, named by `exampleAgent.name` (default +`kagent-tools-example`). It is off by default. The chart then creates: + +- a Harness of the same name that runs the Go ADK runtime from the release's + Harness image catalog, on the `substrateWorkerPool.name` WorkerPool; +- an Agent with an inline template that uses `default-model-config`, includes + the `skills-usage`, `kubernetes-context`, `tool-usage-best-practices`, and + `safety-guardrails` prompts from the `kagent-builtin-prompts` ConfigMap, binds + the chart's `-tool-server` RemoteMCPServer, and selects these skills + from the kagent-tools image at `opt/kagent-tools-skills`: + `k8s-troubleshooting`, `helm-release-management`, `istio-traffic-debugging`, + `argo-rollouts-progressive-delivery`, `cilium-network-debugging`, + `kubescape-security-posture`, and `prometheus-observability`. + +The Agent needs `kagent-tools.enabled: true`. Each skill names the +kagent-tools MCP tools it uses. The Agent binds only those 64 tools, split over +two bindings of the tool server because one binding lists at most 50 tools. The +whole server, with the built-in tools, has more than the 128 tools that OpenAI +and Azure OpenAI accept in one request. The Claude and Codex harnesses accept +each MCP server only once, so this Agent runs on the Go ADK harness only. + +The skills come from the kagent-tools image, pinned by digest. `make +helm-publish` resolves the digest of the kagent-tools version in the chart's +dependencies (`make tools-image-metadata`) and packages it in +`files/kagent-tools-image.json`. A source chart has no packaged digest. With a +source chart, or to use another image, set `exampleAgent.skillsImage`: + +```yaml +exampleAgent: + enabled: true + snapshotLocation: gs:///kagent/ + skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256: +``` + +Without a digest, the chart fails to render. `global.imageRegistry` rewrites +the packaged digest and the override. + +The skills folder ships in kagent-tools releases that include +[kagent-dev/tools#91](https://github.com/kagent-dev/tools/pull/91). Until the +chart depends on such a release, set `exampleAgent.skillsImage` to an image +built from it. + +Notes: + +- The example uses an OCI source only. Git sources currently fail to load in + the agent runtime, and S3 sources are untested. +- Skill selection is not a security boundary. Substrate mounts the whole image + read-only, so the agent can still read the files of skills it did not + select. + +The example replaces the ready-made agents of kagent 0.x. These 0.x behaviors +are not carried forward: + +- one Helm flag per agent (for example `argo-rollouts-agent.enabled`); the chart + installs at most this one example Agent; +- the kgateway, cilium-policy, and promql agents, which have no matching skill; +- the Cilium install, upgrade, and ClusterMesh steps of the cilium-manager + agent; `cilium-network-debugging` covers only debugging; +- the persona text of each agent's system message; +- the Grafana tools of the observability agent (1.x has no Grafana MCP server), + and its call to the promql agent as a sub-agent; +- the `query_documentation` tool named in the helm, istio, and kgateway prompts; +- the A2A skill entries in `a2aConfig.skills`, which have no 1.x field. + ### Using Make ```bash diff --git a/helm/kagent/templates/_helpers.tpl b/helm/kagent/templates/_helpers.tpl index 8774839610..87c743e0e4 100644 --- a/helm/kagent/templates/_helpers.tpl +++ b/helm/kagent/templates/_helpers.tpl @@ -13,6 +13,26 @@ {{- $images | mustToJson -}} {{- end -}} +{{/* +Digest-pinned kagent-tools image whose skills the example Agent mounts. Release +packaging resolves the digest of the kagent-tools version this chart pins. +*/}} +{{- define "kagent.exampleAgentSkillsImage" -}} +{{- $image := .Values.exampleAgent.skillsImage -}} +{{- if not $image -}} + {{- with .Files.Get "files/kagent-tools-image.json" -}} + {{- $image = get (mustFromJson .) "image" -}} + {{- end -}} +{{- end -}} +{{- if not $image -}} + {{- fail "exampleAgent.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgent.skillsImage to @sha256:." -}} +{{- end -}} +{{- if not (regexMatch "^[^[:space:]@]+@sha256:[0-9a-f]{64}$" $image) -}} + {{- fail (printf "exampleAgent.skillsImage must be pinned by sha256 digest: %q" $image) -}} +{{- end -}} +{{- include "kagent.mirroredImage" (dict "root" . "image" $image) -}} +{{- end -}} + {{/* Create a default fully qualified app name. */}} diff --git a/helm/kagent/templates/example-agent.yaml b/helm/kagent/templates/example-agent.yaml new file mode 100644 index 0000000000..11484b7bce --- /dev/null +++ b/helm/kagent/templates/example-agent.yaml @@ -0,0 +1,150 @@ +{{- if .Values.exampleAgent.enabled }} +{{- if not (index .Values "kagent-tools" "enabled") }} +{{- fail "exampleAgent.enabled requires kagent-tools.enabled: the example skills call the kagent-tools MCP server" }} +{{- end }} +{{- if not .Values.exampleAgent.snapshotLocation }} +{{- fail "exampleAgent.snapshotLocation is required: set the snapshot storage location for the example Harness" }} +{{- end }} +{{- $skillsImage := include "kagent.exampleAgentSkillsImage" . }} +{{- $harnessImage := get (include "kagent.builtinHarnessImages" . | fromJson) "kagent" }} +apiVersion: api.kagent.dev/v1alpha3 +kind: Harness +metadata: + name: {{ .Values.exampleAgent.name | quote }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + kagent: {} + {{- with $harnessImage }} + workload: + image: {{ . | quote }} + {{- end }} + substrate: + workerPoolRef: + name: {{ .Values.substrateWorkerPool.name | quote }} + snapshotPolicy: + location: {{ .Values.exampleAgent.snapshotLocation | quote }} +--- +apiVersion: api.kagent.dev/v1alpha3 +kind: Agent +metadata: + name: {{ .Values.exampleAgent.name | quote }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + harnessRef: + name: {{ .Values.exampleAgent.name | quote }} + template: + inline: + description: Kubernetes operations assistant that uses the kagent-tools skills and MCP tools. + modelConfig: + name: {{ include "kagent.defaultModelConfigName" . }} + promptTemplate: + dataSources: + - name: kagent-builtin-prompts + alias: builtin + systemPrompt: | + Answer questions about the Kubernetes cluster where kagent runs. When a + question matches one of your skills, load that skill and follow it. Each + skill names the kagent-tools MCP tools it uses. + + {{`{{include "builtin/skills-usage"}}`}} + + {{`{{include "builtin/kubernetes-context"}}`}} + + {{`{{include "builtin/tool-usage-best-practices"}}`}} + + {{`{{include "builtin/safety-guardrails"}}`}} + # The 64 tools the skills name. The whole server has more than 128 tools + # with the built-ins, which OpenAI and Azure OpenAI reject. One binding + # lists at most 50 tools, so the list is split over two bindings. + tools: + - mcp: + server: + kind: RemoteMCPServer + name: {{ include "kagent.fullname" . }}-tool-server + tools: + - k8s_apply_manifest + - k8s_check_service_connectivity + - k8s_delete_resource + - k8s_describe_resource + - k8s_get_events + - k8s_get_pod_logs + - k8s_get_resource_yaml + - k8s_get_resources + - k8s_patch_resource + - k8s_rollout + - k8s_scale + - helm_get_release + - helm_list_releases + - helm_repo_add + - helm_repo_update + - helm_uninstall + - helm_upgrade + - istio_analyze_cluster_configuration + - istio_apply_waypoint + - istio_delete_waypoint + - istio_list_waypoints + - istio_proxy_config + - istio_proxy_status + - istio_version + - istio_waypoint_status + - istio_ztunnel_config + - argo_pause_rollout + - argo_promote_rollout + - argo_rollouts_list + - argo_set_rollout_image + - argo_verify_argo_rollouts_controller_install + - argo_verify_gateway_plugin + - argo_verify_kubectl_plugin_install + - mcp: + server: + kind: RemoteMCPServer + name: {{ include "kagent.fullname" . }}-tool-server + tools: + - cilium_display_policy_node_information + - cilium_display_selectors + - cilium_flush_ipsec_state + - cilium_get_daemon_status + - cilium_get_endpoint_details + - cilium_get_endpoints_list + - cilium_get_identity_details + - cilium_get_service_information + - cilium_list_bgp_peers + - cilium_list_bgp_routes + - cilium_list_cluster_nodes + - cilium_list_services + - cilium_show_cluster_mesh_status + - cilium_status_and_version + - cilium_uninstall_cilium + - cilium_validate_cilium_network_policies + - kubescape_check_health + - kubescape_get_application_profile + - kubescape_get_configuration_scan + - kubescape_get_network_neighborhood + - kubescape_get_vulnerability_details + - kubescape_list_application_profiles + - kubescape_list_configuration_scans + - kubescape_list_network_neighborhoods + - kubescape_list_vulnerabilities + - kubescape_list_vulnerability_manifests + - prometheus_label_names_tool + - prometheus_promql_tool + - prometheus_query_range_tool + - prometheus_query_tool + - prometheus_targets_tool + plugins: + - source: + oci: {{ $skillsImage | quote }} + path: opt/kagent-tools-skills + skills: + - k8s-troubleshooting + - helm-release-management + - istio-traffic-debugging + - argo-rollouts-progressive-delivery + - cilium-network-debugging + - kubescape-security-posture + - prometheus-observability +{{- end }} diff --git a/helm/kagent/tests/example-agent_test.yaml b/helm/kagent/tests/example-agent_test.yaml new file mode 100644 index 0000000000..7e4fca145a --- /dev/null +++ b/helm/kagent/tests/example-agent_test.yaml @@ -0,0 +1,191 @@ +suite: test example agent +templates: + - example-agent.yaml +release: + name: kagent + namespace: kagent +set: + exampleAgent.enabled: true + exampleAgent.snapshotLocation: gs://bucket/kagent/ + exampleAgent.skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + config.harnessImages.kagent: ghcr.io/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +tests: + - it: should render nothing by default + set: + exampleAgent.enabled: false + asserts: + - hasDocuments: + count: 0 + + - it: should render a Go ADK Harness with the release image + documentSelector: + path: kind + value: Harness + asserts: + - equal: + path: apiVersion + value: api.kagent.dev/v1alpha3 + - equal: + path: metadata.name + value: kagent-tools-example + - equal: + path: metadata.namespace + value: kagent + - equal: + path: spec.kagent + value: {} + - equal: + path: spec.workload.image + value: ghcr.io/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - equal: + path: spec.substrate.workerPoolRef.name + value: kagent-default + - equal: + path: spec.substrate.snapshotPolicy.location + value: gs://bucket/kagent/ + + - it: should omit the workload when the release has no Go ADK image + set: + config.harnessImages.kagent: "" + documentSelector: + path: kind + value: Harness + asserts: + - notExists: + path: spec.workload + + - it: should render one Agent with the skills, model, prompts, and tool server + documentSelector: + path: kind + value: Agent + asserts: + - equal: + path: apiVersion + value: api.kagent.dev/v1alpha3 + - equal: + path: metadata.name + value: kagent-tools-example + - equal: + path: spec.harnessRef.name + value: kagent-tools-example + - equal: + path: spec.template.inline.modelConfig.name + value: default-model-config + - equal: + path: spec.template.inline.promptTemplate.dataSources + value: + - name: kagent-builtin-prompts + alias: builtin + - matchRegex: + path: spec.template.inline.systemPrompt + pattern: '\{\{include "builtin/safety-guardrails"\}\}' + - matchRegex: + path: spec.template.inline.systemPrompt + pattern: '\{\{include "builtin/skills-usage"\}\}' + - lengthEqual: + path: spec.template.inline.tools + count: 2 + - equal: + path: spec.template.inline.tools[0].mcp.server + value: + kind: RemoteMCPServer + name: kagent-tool-server + - equal: + path: spec.template.inline.tools[1].mcp.server + value: + kind: RemoteMCPServer + name: kagent-tool-server + - lengthEqual: + path: spec.template.inline.tools[0].mcp.tools + count: 33 + - lengthEqual: + path: spec.template.inline.tools[1].mcp.tools + count: 31 + - contains: + path: spec.template.inline.tools[0].mcp.tools + content: helm_list_releases + - equal: + path: spec.template.inline.plugins + value: + - source: + oci: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + path: opt/kagent-tools-skills + skills: + - k8s-troubleshooting + - helm-release-management + - istio-traffic-debugging + - argo-rollouts-progressive-delivery + - cilium-network-debugging + - kubescape-security-posture + - prometheus-observability + + - it: should follow the release name, namespace override, and Agent name + release: + name: custom + set: + namespaceOverride: agents + exampleAgent.name: tools-demo + asserts: + - equal: + path: metadata.namespace + value: agents + - equal: + path: metadata.name + value: tools-demo + - equal: + path: spec.harnessRef.name + value: tools-demo + documentSelector: + path: kind + value: Agent + - equal: + path: spec.template.inline.tools[0].mcp.server.name + value: custom-tool-server + documentSelector: + path: kind + value: Agent + + - it: should mirror the skills and Harness images + set: + global.imageRegistry: mirror.example/ + asserts: + - equal: + path: spec.template.inline.plugins[0].source.oci + value: mirror.example/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + documentSelector: + path: kind + value: Agent + - equal: + path: spec.workload.image + value: mirror.example/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + documentSelector: + path: kind + value: Harness + + - it: should fail without a skills image digest + set: + exampleAgent.skillsImage: "" + asserts: + - failedTemplate: + errorMessage: "exampleAgent.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgent.skillsImage to @sha256:." + + - it: should fail when the skills image is not pinned by digest + set: + exampleAgent.skillsImage: ghcr.io/kagent-dev/kagent/tools:0.3.0 + asserts: + - failedTemplate: + errorMessage: 'exampleAgent.skillsImage must be pinned by sha256 digest: "ghcr.io/kagent-dev/kagent/tools:0.3.0"' + + - it: should fail without the kagent-tools server + set: + kagent-tools.enabled: false + asserts: + - failedTemplate: + errorMessage: "exampleAgent.enabled requires kagent-tools.enabled: the example skills call the kagent-tools MCP server" + + - it: should fail without a snapshot location + set: + exampleAgent.snapshotLocation: "" + asserts: + - failedTemplate: + errorMessage: "exampleAgent.snapshotLocation is required: set the snapshot storage location for the example Harness" diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 213877bcb6..e2503c7587 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -23,8 +23,9 @@ global: # air-gap mirror knob: one value redirects all images. Repository and tag stay # per-image. For per-image control, leave this unset and set the per-image # registry keys. Covers this chart's own images: controller, sandbox guest, ui, - # substrateWorkerPool.workerImage, and built-in Harness images - # (both packaged defaults and config.harnessImages overrides). The vendored subcharts + # substrateWorkerPool.workerImage, built-in Harness images + # (both packaged defaults and config.harnessImages overrides), and the example + # Agent skills image (packaged default and exampleAgent.skillsImage). The vendored subcharts # kagent-tools adopts it when its pinned version bumps. imageRegistry: "" # -- Pull secrets for every pod. The chart merges this list (union) into each @@ -770,6 +771,27 @@ kagent-tools: metrics: port: 8085 # Use a different port than the controller service port to avoid duplicate port definitions +# ============================================================================== +# EXAMPLE AGENT +# ============================================================================== +# One Agent with its own Go ADK Harness. It uses the default ModelConfig, the +# kagent-tools MCP server, and the seven skills packaged in the kagent-tools +# image under opt/kagent-tools-skills. Requires kagent-tools.enabled. +exampleAgent: + # -- Install the example Agent and its Harness. + enabled: false + # -- Name of the example Agent and its Harness. + name: kagent-tools-example + # -- Digest-pinned kagent-tools image that holds the skills, for example + # ghcr.io/kagent-dev/kagent/tools@sha256:. Empty uses the digest that + # release packaging resolved for the kagent-tools version this chart pins. + # Source charts have no packaged digest, so set this value to use them. + # global.imageRegistry rewrites both the packaged default and this override. + skillsImage: "" + # -- Snapshot storage location for the example Harness, for example + # gs:///kagent/. Required when enabled. + snapshotLocation: "" + # ============================================================================== # PROXY CONFIGURATION # ============================================================================== diff --git a/scripts/package-release-charts.sh b/scripts/package-release-charts.sh index d0478130a7..67c806d4d6 100644 --- a/scripts/package-release-charts.sh +++ b/scripts/package-release-charts.sh @@ -17,9 +17,34 @@ bash "$root/scripts/harness-image-catalog.sh" "$version" "$metadata_dir" \ "$repository/golang-adk" "$repository/claude-harness" "$repository/codex-harness" \ > "$staging/builtin-harness-images.json" +# make tools-image-metadata records the kagent-tools image digest for the +# example Agent. Without it, the chart can enable that Agent only with an +# explicit exampleAgent.skillsImage. +tools_metadata="$metadata_dir/kagent-tools.json" +if [[ -e "$tools_metadata" ]]; then + # The image tag must be the kagent-tools version the chart depends on, so a + # stale metadata file cannot pin another release's skills. + tools_version="$(yq '(.dependencies // [])[] | select(.name == "kagent-tools") | .version' "$source_dir/kagent/Chart-template.yaml")" + jq -e --arg tools_version "$tools_version" ' + .["containerimage.digest"] as $digest | .["image.name"] as $name | + if ($digest | type) == "string" and ($digest | test("^sha256:[a-f0-9]{64}$")) and + ($name | type) == "string" and $name != "" and + ($tools_version == "" or ($name | split("@")[0] | capture(":(?[^/:]+)$").t) == $tools_version) + then {image: (($name | split("@")[0] | sub(":[^/:]+$"; "")) + "@" + $digest)} + else error("missing or invalid final image digest for kagent-tools") + end' "$tools_metadata" > "$staging/kagent-tools-image.json" || { + echo "Invalid kagent-tools image metadata, or its tag is not kagent-tools $tools_version: $tools_metadata. Run make tools-image-metadata." >&2 + exit 1 + } +fi + cp -R "$source_dir" "$staging/helm" mkdir -p "$staging/helm/kagent/files" mv "$staging/builtin-harness-images.json" "$staging/helm/kagent/files/builtin-harness-images.json" +rm -f "$staging/helm/kagent/files/kagent-tools-image.json" +if [[ -f "$staging/kagent-tools-image.json" ]]; then + mv "$staging/kagent-tools-image.json" "$staging/helm/kagent/files/kagent-tools-image.json" +fi # Reuse local chart preparation, keeping generated files and intermediate archives isolated. make --no-print-directory -j1 -C "$root" helm-version VERSION="$version" \ diff --git a/scripts/package-release-charts_test.sh b/scripts/package-release-charts_test.sh index ab12e178f8..ece888ee6c 100644 --- a/scripts/package-release-charts_test.sh +++ b/scripts/package-release-charts_test.sh @@ -13,6 +13,7 @@ output_dir="$scratch/release charts" version=1.0.0-alpha9 digest="sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" config_digest="sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +tools_digest="sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" mkdir -p "$source_dir/kagent/templates" "$source_dir/kagent/files" "$metadata_dir" "$output_dir" # Use the real controller templates in dependency-free charts. The test invokes @@ -22,13 +23,17 @@ for chart in kagent kagent-crds; do printf 'apiVersion: v2\nname: %s\nversion: ${VERSION}\n' "${chart##*/}" > "$source_dir/$chart/Chart-template.yaml" done cp "$root/helm/kagent/values.yaml" "$source_dir/kagent/values.yaml" -cp "$root/helm/kagent/templates/"*.tpl "$root/helm/kagent/templates/controller-configmap.yaml" "$source_dir/kagent/templates/" +cp "$root/helm/kagent/templates/"*.tpl "$root/helm/kagent/templates/controller-configmap.yaml" \ + "$root/helm/kagent/templates/example-agent.yaml" "$source_dir/kagent/templates/" printf 'stale catalog\n' > "$source_dir/kagent/files/builtin-harness-images.json" +printf 'stale tools image\n' > "$source_dir/kagent/files/kagent-tools-image.json" cp -R "$source_dir" "$scratch/source-before" for image in golang-adk claude-harness codex-harness; do jq -n --arg digest "$digest" --arg config "$config_digest" \ '{"containerimage.digest": $digest, "containerimage.config.digest": $config}' > "$metadata_dir/$image.json" done +jq -n --arg digest "$tools_digest" \ + '{"image.name": "ghcr.io/kagent-dev/kagent/tools:0.3.0", "containerimage.digest": $digest}' > "$metadata_dir/kagent-tools.json" release() { make --no-print-directory -C "$root" VERSION="$version" AWK=awk \ @@ -44,6 +49,15 @@ render_images() { "$scratch/rendered.yaml" > "$scratch/rendered.json" } +render_skills_source() { + local archive="$1" + shift + helm template release "$archive" --show-only templates/example-agent.yaml \ + --set exampleAgent.enabled=true --set exampleAgent.snapshotLocation=gs://bucket/kagent/ "$@" > "$scratch/rendered.yaml" + yq -o=json 'select(.kind == "Agent") | .spec.template.inline.plugins[0].source' \ + "$scratch/rendered.yaml" > "$scratch/rendered.json" +} + expect_failure() { local expected="$1" shift @@ -69,6 +83,20 @@ jq -e --arg digest "$digest" --arg version "$version" ' cp "$scratch/rendered.json" "$scratch/defaults.json" diff -r "$scratch/source-before" "$source_dir" +check="release packages the kagent-tools image digest" +render_skills_source "$archive" +jq -e --arg digest "$tools_digest" \ + '. == {oci: ("ghcr.io/kagent-dev/kagent/tools@" + $digest), path: "opt/kagent-tools-skills"}' \ + "$scratch/rendered.json" >/dev/null + +check="mirror and override the kagent-tools image" +render_skills_source "$archive" --set global.imageRegistry=mirror.example/ +jq -e --arg digest "$tools_digest" '.oci == "mirror.example/kagent-dev/kagent/tools@" + $digest' \ + "$scratch/rendered.json" >/dev/null +render_skills_source "$archive" --set-string "exampleAgent.skillsImage=custom.example/team/tools@$config_digest" +jq -e --arg config "$config_digest" '.oci == "custom.example/team/tools@" + $config' \ + "$scratch/rendered.json" >/dev/null + check="override one image" render_images "$archive" --set-string "config.harnessImages.codex=custom.example/team/codex@$config_digest" jq -e --slurpfile defaults "$scratch/defaults.json" --arg config "$config_digest" \ @@ -117,12 +145,97 @@ mv "$metadata_dir/codex-harness.json" "$scratch/codex-invalid.json" expect_failure codex-harness helm-release cp "$scratch/codex-valid.json" "$metadata_dir/codex-harness.json" +cp "$metadata_dir/kagent-tools.json" "$scratch/kagent-tools-valid.json" +for invalid in '{}' '{"image.name":"ghcr.io/kagent-dev/kagent/tools:0.3.0","containerimage.digest":"sha256:bad"}' \ + '{"containerimage.digest":"'"$tools_digest"'"}' '{broken'; do + check="invalid kagent-tools metadata: $invalid" + printf '%s\n' "$invalid" > "$metadata_dir/kagent-tools.json" + expect_failure kagent-tools helm-release +done +cp "$scratch/kagent-tools-valid.json" "$metadata_dir/kagent-tools.json" +# The check runs before packaging, so the dependency is never fetched. +check="kagent-tools metadata for another kagent-tools version" +cp "$source_dir/kagent/Chart-template.yaml" "$scratch/kagent-chart-template.yaml" +printf 'dependencies:\n - name: kagent-tools\n version: 0.3.1\n' >> "$source_dir/kagent/Chart-template.yaml" +expect_failure 'its tag is not kagent-tools 0.3.1' helm-release +cp "$scratch/kagent-chart-template.yaml" "$source_dir/kagent/Chart-template.yaml" + check="packaging failure preserves previous archives" mv "$source_dir/kagent/Chart-template.yaml" "$scratch/Chart-template.yaml" expect_failure kagent/Chart-template.yaml helm-release mv "$scratch/Chart-template.yaml" "$source_dir/kagent/Chart-template.yaml" diff -r "$scratch/source-before" "$source_dir" +# The stale source file would fail to parse, so the message also proves that +# packaging dropped it. +check="release without kagent-tools metadata requires an explicit skills image" +mv "$metadata_dir/kagent-tools.json" "$scratch/kagent-tools.json" +release helm-release +if helm template release "$archive" --show-only templates/example-agent.yaml \ + --set exampleAgent.enabled=true --set exampleAgent.snapshotLocation=gs://bucket/kagent/ > "$scratch/rendered.yaml" 2> "$scratch/command.log"; then + exit 1 +fi +[[ "$(< "$scratch/command.log")" == *'requires the kagent-tools image digest'* ]] +mv "$scratch/kagent-tools.json" "$metadata_dir/kagent-tools.json" + +# kagent-tools is released from its own repository, so its digest is the one +# registry lookup in release packaging. A fixture stands in for the registry. +mkdir -p "$scratch/tools-bin" "$scratch/tools-source/kagent" +cat > "$scratch/tools-bin/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +echo "docker $*" >> "$CONTAINER_LOG" +[[ "$1 $2 $3" == "buildx imagetools inspect" ]] +printf 'Name: %s\nMediaType: application/vnd.oci.image.index.v1+json\nDigest: %s\n' "$4" "$TOOLS_FIXTURE_DIGEST" +EOF +chmod +x "$scratch/tools-bin/docker" +resolve_tools() { + PATH="$scratch/tools-bin:$PATH" CONTAINER_LOG="$scratch/tools.log" release tools-image-metadata \ + HELM_SOURCE_DIR="$scratch/tools-source" RUNTIME_IMAGE_METADATA_DIR="$scratch/tools-metadata" "$@" +} +printf 'apiVersion: v2\nname: kagent\nversion: ${VERSION}\ndependencies:\n - name: kagent-tools\n version: %s\n' \ + 0.3.1 > "$scratch/tools-source/kagent/Chart-template.yaml" + +check="tools-image-metadata resolves the pinned kagent-tools version" +TOOLS_FIXTURE_DIGEST="$tools_digest" resolve_tools TOOLS_IMAGE_REPOSITORY=registry.example/team/tools +grep -qx 'docker buildx imagetools inspect registry.example/team/tools:0.3.1' "$scratch/tools.log" +jq -e --arg digest "$tools_digest" \ + '. == {"image.name": "registry.example/team/tools:0.3.1", "containerimage.digest": $digest}' \ + "$scratch/tools-metadata/kagent-tools.json" >/dev/null + +check="tools-image-metadata records no digest for kagent-tools without skills" +printf 'apiVersion: v2\nname: kagent\nversion: ${VERSION}\ndependencies:\n - name: kagent-tools\n version: %s\n' \ + 0.3.0 > "$scratch/tools-source/kagent/Chart-template.yaml" +: > "$scratch/tools.log" +TOOLS_FIXTURE_DIGEST="$tools_digest" resolve_tools +test ! -e "$scratch/tools-metadata/kagent-tools.json" +test ! -s "$scratch/tools.log" +printf 'apiVersion: v2\nname: kagent\nversion: ${VERSION}\ndependencies:\n - name: kagent-tools\n version: %s\n' \ + 0.3.1 > "$scratch/tools-source/kagent/Chart-template.yaml" +TOOLS_FIXTURE_DIGEST="$tools_digest" resolve_tools + +check="tools-image-metadata requires a resolved digest" +rm "$scratch/tools-metadata/kagent-tools.json" +if TOOLS_FIXTURE_DIGEST= resolve_tools; then + exit 1 +fi +[[ "$(< "$scratch/command.log")" == *'could not resolve the digest of ghcr.io/kagent-dev/kagent/tools:0.3.1'* ]] +test ! -e "$scratch/tools-metadata/kagent-tools.json" + +check="tools-image-metadata requires an exact kagent-tools version" +printf 'apiVersion: v2\nname: kagent\nversion: ${VERSION}\ndependencies:\n - name: kagent-tools\n version: "~0.3.0"\n' \ + > "$scratch/tools-source/kagent/Chart-template.yaml" +if TOOLS_FIXTURE_DIGEST="$tools_digest" resolve_tools; then + exit 1 +fi +[[ "$(< "$scratch/command.log")" == *'must pin an exact version'* ]] + +check="helm-publish resolves the kagent-tools digest before packaging" +release -n helm-publish +tools_line="$(grep -n 'tools-image-metadata.sh' "$scratch/command.log" | head -1 | cut -d: -f1)" +package_line="$(grep -n 'package-release-charts.sh' "$scratch/command.log" | head -1 | cut -d: -f1)" +test -n "$tools_line" && test -n "$package_line" && ((tools_line < package_line)) + # Run the real development Make targets with container-tool fixtures. Docker's # final Buildx digest and Podman's pushed digest must both reach the installed # source chart, including when the image names and tags are overridden. diff --git a/scripts/tools-image-metadata.sh b/scripts/tools-image-metadata.sh new file mode 100644 index 0000000000..fee70c8786 --- /dev/null +++ b/scripts/tools-image-metadata.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Called by make tools-image-metadata. kagent-tools is released from its own +# repository, so no build here records its digest; resolve it from the registry. +# The output uses the Buildx metadata keys that release packaging validates. +chart_template="${1:?kagent Chart-template.yaml is required}" +repository="${2:?kagent-tools image repository is required}" + +command -v yq >/dev/null || { echo "tools-image-metadata.sh needs yq (mikefarah yq v4)" >&2; exit 1; } +version="$(yq -e '.dependencies[] | select(.name == "kagent-tools") | .version' "$chart_template")" +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "The kagent-tools dependency in $chart_template must pin an exact version, got: $version" >&2 + exit 1 +fi +# kagent-tools 0.3.0 and earlier ship no opt/kagent-tools-skills folder. Record +# no digest for them, so the example Agent needs an explicit skillsImage. +last_without_skills="0.3.0" +if [[ "$(printf '%s\n%s\n' "$version" "$last_without_skills" | sort -V | tail -n1)" == "$last_without_skills" ]]; then + echo "kagent-tools $version has no skills folder; recording no digest" >&2 + exit 0 +fi +# The kagent-tools chart tags its image with the chart version. +image="$repository:$version" + +# Read all output so Docker does not get a broken pipe under pipefail. +digest="$(docker buildx imagetools inspect "$image" | awk '$1 == "Digest:" { print $2 }')" +jq -en --arg image "$image" --arg digest "$digest" ' + if $digest | test("^sha256:[a-f0-9]{64}$") + then {"image.name": $image, "containerimage.digest": $digest} + else error("could not resolve the digest of " + $image) + end' From 720ce4194a1cf2bac125f5f80dbd1fd36167e4ab Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 14:06:20 -0400 Subject: [PATCH 02/12] feat(helm): split the example into three Agents One binding lists at most 50 tools, Claude and Codex accept each MCP server once, and OpenAI accepts at most 128 tools per request. Three Agents with one binding each fit every harness. Signed-off-by: Danielle Mustillo --- Makefile | 2 +- .../configuration-and-compilation.md | 4 +- docs/env.md | 2 +- go/core/pkg/env/testing.go | 2 +- go/core/test/e2e/README.md | 11 +- go/core/test/e2e/example_agent_skills_test.go | 24 ++- helm/README.md | 64 ++++---- helm/kagent/templates/_helpers.tpl | 10 +- helm/kagent/templates/example-agent.yaml | 150 ------------------ helm/kagent/templates/example-agents.yaml | 121 ++++++++++++++ ...ent_test.yaml => example-agents_test.yaml} | 126 +++++++++------ helm/kagent/values.yaml | 20 +-- scripts/package-release-charts.sh | 4 +- scripts/package-release-charts_test.sh | 12 +- scripts/tools-image-metadata.sh | 2 +- 15 files changed, 289 insertions(+), 265 deletions(-) delete mode 100644 helm/kagent/templates/example-agent.yaml create mode 100644 helm/kagent/templates/example-agents.yaml rename helm/kagent/tests/{example-agent_test.yaml => example-agents_test.yaml} (57%) diff --git a/Makefile b/Makefile index c8254c32a6..a62b9dfef4 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ HELM_REPO ?= oci://ghcr.io/kagent-dev HELM_DIST_FOLDER ?= dist HELM_SOURCE_DIR ?= helm RUNTIME_IMAGE_REPOSITORY ?= ghcr.io/kagent-dev/kagent -# The kagent-tools image whose skills the chart's example Agent mounts. Its tag +# The kagent-tools image whose skills the chart's example Agents mount. Its tag # is the kagent-tools chart version that helm/kagent depends on. TOOLS_IMAGE_REPOSITORY ?= ghcr.io/kagent-dev/kagent/tools # Release packaging requires an explicit metadata directory; local builds have a default. diff --git a/docs/architecture/configuration-and-compilation.md b/docs/architecture/configuration-and-compilation.md index 704eb594c5..4ad7b9aa12 100644 --- a/docs/architecture/configuration-and-compilation.md +++ b/docs/architecture/configuration-and-compilation.md @@ -95,14 +95,14 @@ Local installations can override individual defaults with `config.harnessImages`; `global.imageRegistry` rewrites both packaged defaults and explicit overrides. -The chart's optional example Agent mounts skills from the kagent-tools image. +The chart's optional example Agents mount skills from the kagent-tools image. That image is released from its own repository, so no build here records its digest. Before packaging, `make helm-publish` runs `make tools-image-metadata`, which resolves the digest of the kagent-tools version in the chart's dependencies from the registry and writes it to `RUNTIME_IMAGE_METADATA_DIR`. `make helm-release` validates that metadata when present and packages `files/kagent-tools-image.json`. Without it, the chart renders the example -Agent only with an explicit `exampleAgent.skillsImage`. +Agents only with an explicit `exampleAgents.skillsImage`. The outer compiler sets the selected image on the compiled revision before hashing it. Adapters receive the requested Harness spec; provenance records that spec diff --git a/docs/env.md b/docs/env.md index 3ae2dae599..8bc030b020 100644 --- a/docs/env.md +++ b/docs/env.md @@ -180,7 +180,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_E2E_RUN_UPGRADE_TESTS` | String | `false` | Run upgrade integration tests when exactly true. | | `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | -| `KAGENT_E2E_TOOLS_SKILLS_IMAGE` | String | `(none)` | Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agent E2E test. That test is skipped when unset. | +| `KAGENT_E2E_TOOLS_SKILLS_IMAGE` | String | `(none)` | Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agents E2E test. That test is skipped when unset. | | `KAGENT_E2E_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50. | | `KAGENT_E2E_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | | `KAGENT_E2E_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index e248c97e16..9a5bf21f58 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -56,7 +56,7 @@ var ( _ = RegisterStringVar("KAGENT_E2E_BYO_IMAGE", "", "Digest-pinned BYO image substituted into E2E lifecycle manifests.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_CLAUDE_IMAGE", "", "Digest-pinned Claude harness image substituted into E2E lifecycle manifests.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_CODEX_IMAGE", "", "Digest-pinned Codex harness image substituted into E2E lifecycle manifests.", ComponentTesting) - E2EToolsSkillsImage = RegisterStringVar("KAGENT_E2E_TOOLS_SKILLS_IMAGE", "", "Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agent E2E test. That test is skipped when unset.", ComponentTesting) + E2EToolsSkillsImage = RegisterStringVar("KAGENT_E2E_TOOLS_SKILLS_IMAGE", "", "Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agents E2E test. That test is skipped when unset.", ComponentTesting) E2ESandboxNamespace = RegisterStringVar("KAGENT_E2E_SANDBOX_NAMESPACE", "kagent", "Namespace for sandbox E2E resources.", ComponentTesting) E2ESandboxWorkerPool = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) E2EOTLPListenAddress = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) diff --git a/go/core/test/e2e/README.md b/go/core/test/e2e/README.md index b05122115a..397263a2bb 100644 --- a/go/core/test/e2e/README.md +++ b/go/core/test/e2e/README.md @@ -7,11 +7,12 @@ the three `config.harnessImages` defaults matching the explicit Harness fixtures. The test checks runtime readiness, effective-image status, and that the controller leaves the user spec unchanged. -`TestExampleAgentToolsSkills` runs the chart's example Agent behavior on every -harness with a mock model. It needs `KAGENT_E2E_TOOLS_SKILLS_IMAGE`, a -digest-pinned kagent-tools image that contains the `opt/kagent-tools-skills` -Agent Plugin, in the local test registry; the test is skipped without it. It -checks that the seven skills are advertised and that `helm-release-management` +`TestExampleAgentToolsSkills` runs the chart's cluster-ops example Agent +behavior on every harness with a mock model. It needs +`KAGENT_E2E_TOOLS_SKILLS_IMAGE`, a digest-pinned kagent-tools image that +contains the `opt/kagent-tools-skills` Agent Plugin, in the local test registry; +the test is skipped without it. It checks that the Agent's four skills are +advertised, that the other three are not, and that `helm-release-management` loads. It omits the tool server binding because the test install does not run kagent-tools. diff --git a/go/core/test/e2e/example_agent_skills_test.go b/go/core/test/e2e/example_agent_skills_test.go index b8bd46f4c1..7699b0fac6 100644 --- a/go/core/test/e2e/example_agent_skills_test.go +++ b/go/core/test/e2e/example_agent_skills_test.go @@ -16,21 +16,26 @@ import ( kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" ) -// exampleAgentSkills are the kagent-tools skills that the chart's example Agent -// (helm/kagent/templates/example-agent.yaml) selects. +// exampleAgentSkills are the kagent-tools skills that the chart's cluster-ops +// example Agent (helm/kagent/templates/example-agents.yaml) selects. var exampleAgentSkills = []string{ "k8s-troubleshooting", "helm-release-management", - "istio-traffic-debugging", "argo-rollouts-progressive-delivery", + "prometheus-observability", +} + +// exampleAgentOtherSkills are in the same image but belong to the other +// example Agents. +var exampleAgentOtherSkills = []string{ + "istio-traffic-debugging", "cilium-network-debugging", "kubescape-security-posture", - "prometheus-observability", } -// TestExampleAgentToolsSkills runs the chart's example Agent behavior on every -// harness with a mock model: the kagent-tools image as an Agent Plugin, its -// seven skills, and the built-in prompt library. The chart's tool server +// TestExampleAgentToolsSkills runs the chart's cluster-ops example Agent +// behavior on every harness with a mock model: the kagent-tools image as an +// Agent Plugin, four of its skills, and the built-in prompt library. The chart's tool server // binding is left out because the E2E install does not run kagent-tools. func TestExampleAgentToolsSkills(t *testing.T) { t.Parallel() @@ -48,7 +53,7 @@ func TestExampleAgentToolsSkills(t *testing.T) { ObjectMeta: metav1.ObjectMeta{GenerateName: "example-skills-", Namespace: "kagent", Labels: harness.labels()}, Spec: v1alpha3.AgentTemplateSpec{ ModelConfig: &corev1.LocalObjectReference{Name: config.Name}, - Description: "Kubernetes operations assistant that uses the kagent-tools skills and MCP tools.", + Description: "Kubernetes workload, Helm release, Argo Rollouts, and Prometheus metrics assistant.", PromptTemplate: &v1alpha3.AgentTemplatePromptTemplateSpec{ DataSources: []v1alpha3.AgentTemplatePromptSource{{Name: "kagent-builtin-prompts", Alias: "builtin"}}, }, @@ -79,6 +84,9 @@ func TestExampleAgentToolsSkills(t *testing.T) { for _, skill := range exampleAgentSkills { require.Contains(t, first, skill, "skill must be advertised to the model") } + for _, skill := range exampleAgentOtherSkills { + require.NotContains(t, first, skill, "unselected skill must not be advertised") + } require.Contains(t, first, "## Safety Guidelines", "built-in prompts must be included") // Discovery exposes metadata only. The skill body names its MCP tools. require.NotContains(t, first, "helm_list_releases") diff --git a/helm/README.md b/helm/README.md index 073f844b77..c2adc69513 100644 --- a/helm/README.md +++ b/helm/README.md @@ -231,38 +231,44 @@ release namespace. **Note**: MicroVM requires a `microvm` SandboxConfig, runtime assets, and KVM-capable workers. kagent does not install these prerequisites. -#### Example Agent - -Set `exampleAgent.enabled: true` and `exampleAgent.snapshotLocation` to install -one example Agent, named by `exampleAgent.name` (default -`kagent-tools-example`). It is off by default. The chart then creates: - -- a Harness of the same name that runs the Go ADK runtime from the release's - Harness image catalog, on the `substrateWorkerPool.name` WorkerPool; -- an Agent with an inline template that uses `default-model-config`, includes - the `skills-usage`, `kubernetes-context`, `tool-usage-best-practices`, and - `safety-guardrails` prompts from the `kagent-builtin-prompts` ConfigMap, binds - the chart's `-tool-server` RemoteMCPServer, and selects these skills - from the kagent-tools image at `opt/kagent-tools-skills`: - `k8s-troubleshooting`, `helm-release-management`, `istio-traffic-debugging`, - `argo-rollouts-progressive-delivery`, `cilium-network-debugging`, - `kubescape-security-posture`, and `prometheus-observability`. - -The Agent needs `kagent-tools.enabled: true`. Each skill names the -kagent-tools MCP tools it uses. The Agent binds only those 64 tools, split over -two bindings of the tool server because one binding lists at most 50 tools. The -whole server, with the built-in tools, has more than the 128 tools that OpenAI -and Azure OpenAI accept in one request. The Claude and Codex harnesses accept -each MCP server only once, so this Agent runs on the Go ADK harness only. +#### Example Agents + +Set `exampleAgents.enabled: true` and `exampleAgents.snapshotLocation` to +install three example Agents. They are off by default. The chart then creates: + +- a Harness named `-example` (default `kagent-tools-example`) that + runs the Go ADK runtime from the release's Harness image catalog, on the + `substrateWorkerPool.name` WorkerPool; +- three Agents on that Harness, each with skills from the kagent-tools image at + `opt/kagent-tools-skills`: + + | Agent | Skills | Tools | + |---|---|---| + | `-cluster-ops` | `k8s-troubleshooting`, `helm-release-management`, `argo-rollouts-progressive-delivery`, `prometheus-observability` | 29 | + | `-networking` | `istio-traffic-debugging`, `cilium-network-debugging` | 27 | + | `-security` | `kubescape-security-posture` | 10 | + +Each Agent uses `default-model-config` and includes the `skills-usage`, +`kubernetes-context`, `tool-usage-best-practices`, and `safety-guardrails` +prompts from the `kagent-builtin-prompts` ConfigMap. Each skill names the +kagent-tools MCP tools it uses, and each Agent binds only those tools from the +chart's `-tool-server` RemoteMCPServer, in one binding. The Agents need +`kagent-tools.enabled: true`. + +The skills are split over three Agents to keep every Agent within three limits: +one binding lists at most 50 tools, the Claude and Codex harnesses accept each +MCP server only once, and OpenAI and Azure OpenAI accept at most 128 tools in +one request. The seven skills together name 64 tools, and the whole tool server +has more than 128. The skills come from the kagent-tools image, pinned by digest. `make helm-publish` resolves the digest of the kagent-tools version in the chart's dependencies (`make tools-image-metadata`) and packages it in `files/kagent-tools-image.json`. A source chart has no packaged digest. With a -source chart, or to use another image, set `exampleAgent.skillsImage`: +source chart, or to use another image, set `exampleAgents.skillsImage`: ```yaml -exampleAgent: +exampleAgents: enabled: true snapshotLocation: gs:///kagent/ skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256: @@ -273,7 +279,7 @@ the packaged digest and the override. The skills folder ships in kagent-tools releases that include [kagent-dev/tools#91](https://github.com/kagent-dev/tools/pull/91). Until the -chart depends on such a release, set `exampleAgent.skillsImage` to an image +chart depends on such a release, set `exampleAgents.skillsImage` to an image built from it. Notes: @@ -284,11 +290,11 @@ Notes: read-only, so the agent can still read the files of skills it did not select. -The example replaces the ready-made agents of kagent 0.x. These 0.x behaviors +The example Agents replace the ready-made agents of kagent 0.x. These 0.x behaviors are not carried forward: -- one Helm flag per agent (for example `argo-rollouts-agent.enabled`); the chart - installs at most this one example Agent; +- one Helm flag per agent (for example `argo-rollouts-agent.enabled`); one + flag installs all three example Agents; - the kgateway, cilium-policy, and promql agents, which have no matching skill; - the Cilium install, upgrade, and ClusterMesh steps of the cilium-manager agent; `cilium-network-debugging` covers only debugging; diff --git a/helm/kagent/templates/_helpers.tpl b/helm/kagent/templates/_helpers.tpl index 87c743e0e4..724c2b8156 100644 --- a/helm/kagent/templates/_helpers.tpl +++ b/helm/kagent/templates/_helpers.tpl @@ -14,21 +14,21 @@ {{- end -}} {{/* -Digest-pinned kagent-tools image whose skills the example Agent mounts. Release +Digest-pinned kagent-tools image whose skills the example Agents mount. Release packaging resolves the digest of the kagent-tools version this chart pins. */}} -{{- define "kagent.exampleAgentSkillsImage" -}} -{{- $image := .Values.exampleAgent.skillsImage -}} +{{- define "kagent.exampleAgentsSkillsImage" -}} +{{- $image := .Values.exampleAgents.skillsImage -}} {{- if not $image -}} {{- with .Files.Get "files/kagent-tools-image.json" -}} {{- $image = get (mustFromJson .) "image" -}} {{- end -}} {{- end -}} {{- if not $image -}} - {{- fail "exampleAgent.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgent.skillsImage to @sha256:." -}} + {{- fail "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgents.skillsImage to @sha256:." -}} {{- end -}} {{- if not (regexMatch "^[^[:space:]@]+@sha256:[0-9a-f]{64}$" $image) -}} - {{- fail (printf "exampleAgent.skillsImage must be pinned by sha256 digest: %q" $image) -}} + {{- fail (printf "exampleAgents.skillsImage must be pinned by sha256 digest: %q" $image) -}} {{- end -}} {{- include "kagent.mirroredImage" (dict "root" . "image" $image) -}} {{- end -}} diff --git a/helm/kagent/templates/example-agent.yaml b/helm/kagent/templates/example-agent.yaml deleted file mode 100644 index 11484b7bce..0000000000 --- a/helm/kagent/templates/example-agent.yaml +++ /dev/null @@ -1,150 +0,0 @@ -{{- if .Values.exampleAgent.enabled }} -{{- if not (index .Values "kagent-tools" "enabled") }} -{{- fail "exampleAgent.enabled requires kagent-tools.enabled: the example skills call the kagent-tools MCP server" }} -{{- end }} -{{- if not .Values.exampleAgent.snapshotLocation }} -{{- fail "exampleAgent.snapshotLocation is required: set the snapshot storage location for the example Harness" }} -{{- end }} -{{- $skillsImage := include "kagent.exampleAgentSkillsImage" . }} -{{- $harnessImage := get (include "kagent.builtinHarnessImages" . | fromJson) "kagent" }} -apiVersion: api.kagent.dev/v1alpha3 -kind: Harness -metadata: - name: {{ .Values.exampleAgent.name | quote }} - namespace: {{ include "kagent.namespace" . }} - labels: - {{- include "kagent.labels" . | nindent 4 }} -spec: - kagent: {} - {{- with $harnessImage }} - workload: - image: {{ . | quote }} - {{- end }} - substrate: - workerPoolRef: - name: {{ .Values.substrateWorkerPool.name | quote }} - snapshotPolicy: - location: {{ .Values.exampleAgent.snapshotLocation | quote }} ---- -apiVersion: api.kagent.dev/v1alpha3 -kind: Agent -metadata: - name: {{ .Values.exampleAgent.name | quote }} - namespace: {{ include "kagent.namespace" . }} - labels: - {{- include "kagent.labels" . | nindent 4 }} -spec: - harnessRef: - name: {{ .Values.exampleAgent.name | quote }} - template: - inline: - description: Kubernetes operations assistant that uses the kagent-tools skills and MCP tools. - modelConfig: - name: {{ include "kagent.defaultModelConfigName" . }} - promptTemplate: - dataSources: - - name: kagent-builtin-prompts - alias: builtin - systemPrompt: | - Answer questions about the Kubernetes cluster where kagent runs. When a - question matches one of your skills, load that skill and follow it. Each - skill names the kagent-tools MCP tools it uses. - - {{`{{include "builtin/skills-usage"}}`}} - - {{`{{include "builtin/kubernetes-context"}}`}} - - {{`{{include "builtin/tool-usage-best-practices"}}`}} - - {{`{{include "builtin/safety-guardrails"}}`}} - # The 64 tools the skills name. The whole server has more than 128 tools - # with the built-ins, which OpenAI and Azure OpenAI reject. One binding - # lists at most 50 tools, so the list is split over two bindings. - tools: - - mcp: - server: - kind: RemoteMCPServer - name: {{ include "kagent.fullname" . }}-tool-server - tools: - - k8s_apply_manifest - - k8s_check_service_connectivity - - k8s_delete_resource - - k8s_describe_resource - - k8s_get_events - - k8s_get_pod_logs - - k8s_get_resource_yaml - - k8s_get_resources - - k8s_patch_resource - - k8s_rollout - - k8s_scale - - helm_get_release - - helm_list_releases - - helm_repo_add - - helm_repo_update - - helm_uninstall - - helm_upgrade - - istio_analyze_cluster_configuration - - istio_apply_waypoint - - istio_delete_waypoint - - istio_list_waypoints - - istio_proxy_config - - istio_proxy_status - - istio_version - - istio_waypoint_status - - istio_ztunnel_config - - argo_pause_rollout - - argo_promote_rollout - - argo_rollouts_list - - argo_set_rollout_image - - argo_verify_argo_rollouts_controller_install - - argo_verify_gateway_plugin - - argo_verify_kubectl_plugin_install - - mcp: - server: - kind: RemoteMCPServer - name: {{ include "kagent.fullname" . }}-tool-server - tools: - - cilium_display_policy_node_information - - cilium_display_selectors - - cilium_flush_ipsec_state - - cilium_get_daemon_status - - cilium_get_endpoint_details - - cilium_get_endpoints_list - - cilium_get_identity_details - - cilium_get_service_information - - cilium_list_bgp_peers - - cilium_list_bgp_routes - - cilium_list_cluster_nodes - - cilium_list_services - - cilium_show_cluster_mesh_status - - cilium_status_and_version - - cilium_uninstall_cilium - - cilium_validate_cilium_network_policies - - kubescape_check_health - - kubescape_get_application_profile - - kubescape_get_configuration_scan - - kubescape_get_network_neighborhood - - kubescape_get_vulnerability_details - - kubescape_list_application_profiles - - kubescape_list_configuration_scans - - kubescape_list_network_neighborhoods - - kubescape_list_vulnerabilities - - kubescape_list_vulnerability_manifests - - prometheus_label_names_tool - - prometheus_promql_tool - - prometheus_query_range_tool - - prometheus_query_tool - - prometheus_targets_tool - plugins: - - source: - oci: {{ $skillsImage | quote }} - path: opt/kagent-tools-skills - skills: - - k8s-troubleshooting - - helm-release-management - - istio-traffic-debugging - - argo-rollouts-progressive-delivery - - cilium-network-debugging - - kubescape-security-posture - - prometheus-observability -{{- end }} diff --git a/helm/kagent/templates/example-agents.yaml b/helm/kagent/templates/example-agents.yaml new file mode 100644 index 0000000000..e0b9ee3815 --- /dev/null +++ b/helm/kagent/templates/example-agents.yaml @@ -0,0 +1,121 @@ +{{- if .Values.exampleAgents.enabled }} +{{- if not (index .Values "kagent-tools" "enabled") }} +{{- fail "exampleAgents.enabled requires kagent-tools.enabled: the example skills call the kagent-tools MCP server" }} +{{- end }} +{{- if not .Values.exampleAgents.snapshotLocation }} +{{- fail "exampleAgents.snapshotLocation is required: set the snapshot storage location for the example Harness" }} +{{- end }} +{{- $skillsImage := include "kagent.exampleAgentsSkillsImage" . }} +{{- $harnessImage := get (include "kagent.builtinHarnessImages" . | fromJson) "kagent" }} +{{- $prefix := .Values.exampleAgents.namePrefix }} +{{- $toolServer := printf "%s-tool-server" (include "kagent.fullname" .) }} +{{- /* +Each Agent binds only the tools its skills name, in one binding. Claude and +Codex accept each MCP server once, one binding lists at most 50 tools, and +OpenAI and Azure OpenAI accept at most 128 tools in one request. +*/}} +{{- $agents := list + (dict + "name" "cluster-ops" + "description" "Kubernetes workload, Helm release, Argo Rollouts, and Prometheus metrics assistant." + "skills" (list "k8s-troubleshooting" "helm-release-management" "argo-rollouts-progressive-delivery" "prometheus-observability") + "tools" (list + "k8s_apply_manifest" "k8s_check_service_connectivity" "k8s_delete_resource" "k8s_describe_resource" + "k8s_get_events" "k8s_get_pod_logs" "k8s_get_resource_yaml" "k8s_get_resources" "k8s_patch_resource" + "k8s_rollout" "k8s_scale" + "helm_get_release" "helm_list_releases" "helm_repo_add" "helm_repo_update" "helm_uninstall" "helm_upgrade" + "argo_pause_rollout" "argo_promote_rollout" "argo_rollouts_list" "argo_set_rollout_image" + "argo_verify_argo_rollouts_controller_install" "argo_verify_gateway_plugin" "argo_verify_kubectl_plugin_install" + "prometheus_label_names_tool" "prometheus_promql_tool" "prometheus_query_range_tool" "prometheus_query_tool" + "prometheus_targets_tool")) + (dict + "name" "networking" + "description" "Istio service mesh and Cilium networking assistant." + "skills" (list "istio-traffic-debugging" "cilium-network-debugging") + "tools" (list + "k8s_apply_manifest" "k8s_get_resource_yaml" + "istio_analyze_cluster_configuration" "istio_apply_waypoint" "istio_delete_waypoint" "istio_list_waypoints" + "istio_proxy_config" "istio_proxy_status" "istio_version" "istio_waypoint_status" "istio_ztunnel_config" + "cilium_display_policy_node_information" "cilium_display_selectors" "cilium_flush_ipsec_state" + "cilium_get_daemon_status" "cilium_get_endpoint_details" "cilium_get_endpoints_list" + "cilium_get_identity_details" "cilium_get_service_information" "cilium_list_bgp_peers" + "cilium_list_bgp_routes" "cilium_list_cluster_nodes" "cilium_list_services" + "cilium_show_cluster_mesh_status" "cilium_status_and_version" "cilium_uninstall_cilium" + "cilium_validate_cilium_network_policies")) + (dict + "name" "security" + "description" "Kubescape security posture assistant." + "skills" (list "kubescape-security-posture") + "tools" (list + "kubescape_check_health" "kubescape_get_application_profile" "kubescape_get_configuration_scan" + "kubescape_get_network_neighborhood" "kubescape_get_vulnerability_details" + "kubescape_list_application_profiles" "kubescape_list_configuration_scans" + "kubescape_list_network_neighborhoods" "kubescape_list_vulnerabilities" + "kubescape_list_vulnerability_manifests")) +}} +apiVersion: api.kagent.dev/v1alpha3 +kind: Harness +metadata: + name: {{ printf "%s-example" $prefix | quote }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + kagent: {} + {{- with $harnessImage }} + workload: + image: {{ . | quote }} + {{- end }} + substrate: + workerPoolRef: + name: {{ .Values.substrateWorkerPool.name | quote }} + snapshotPolicy: + location: {{ .Values.exampleAgents.snapshotLocation | quote }} +{{- range $agents }} +--- +apiVersion: api.kagent.dev/v1alpha3 +kind: Agent +metadata: + name: {{ printf "%s-%s" $prefix .name | quote }} + namespace: {{ include "kagent.namespace" $ }} + labels: + {{- include "kagent.labels" $ | nindent 4 }} +spec: + harnessRef: + name: {{ printf "%s-example" $prefix | quote }} + template: + inline: + description: {{ .description | quote }} + modelConfig: + name: {{ include "kagent.defaultModelConfigName" $ }} + promptTemplate: + dataSources: + - name: kagent-builtin-prompts + alias: builtin + systemPrompt: | + Answer questions about the Kubernetes cluster where kagent runs. When a + question matches one of your skills, load that skill and follow it. Each + skill names the kagent-tools MCP tools it uses. + + {{`{{include "builtin/skills-usage"}}`}} + + {{`{{include "builtin/kubernetes-context"}}`}} + + {{`{{include "builtin/tool-usage-best-practices"}}`}} + + {{`{{include "builtin/safety-guardrails"}}`}} + tools: + - mcp: + server: + kind: RemoteMCPServer + name: {{ $toolServer }} + tools: + {{- toYaml .tools | nindent 14 }} + plugins: + - source: + oci: {{ $skillsImage | quote }} + path: opt/kagent-tools-skills + skills: + {{- toYaml .skills | nindent 12 }} +{{- end }} +{{- end }} diff --git a/helm/kagent/tests/example-agent_test.yaml b/helm/kagent/tests/example-agents_test.yaml similarity index 57% rename from helm/kagent/tests/example-agent_test.yaml rename to helm/kagent/tests/example-agents_test.yaml index 7e4fca145a..8f8ca2cac6 100644 --- a/helm/kagent/tests/example-agent_test.yaml +++ b/helm/kagent/tests/example-agents_test.yaml @@ -1,22 +1,27 @@ -suite: test example agent +suite: test example agents templates: - - example-agent.yaml + - example-agents.yaml release: name: kagent namespace: kagent set: - exampleAgent.enabled: true - exampleAgent.snapshotLocation: gs://bucket/kagent/ - exampleAgent.skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + exampleAgents.enabled: true + exampleAgents.snapshotLocation: gs://bucket/kagent/ + exampleAgents.skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd config.harnessImages.kagent: ghcr.io/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa tests: - it: should render nothing by default set: - exampleAgent.enabled: false + exampleAgents.enabled: false asserts: - hasDocuments: count: 0 + - it: should render one Harness and three Agents + asserts: + - hasDocuments: + count: 4 + - it: should render a Go ADK Harness with the release image documentSelector: path: kind @@ -54,17 +59,15 @@ tests: - notExists: path: spec.workload - - it: should render one Agent with the skills, model, prompts, and tool server + - it: should give every Agent the Harness, model, prompts, skills image, and one tool server binding documentSelector: path: kind value: Agent + matchMany: true asserts: - equal: path: apiVersion value: api.kagent.dev/v1alpha3 - - equal: - path: metadata.name - value: kagent-tools-example - equal: path: spec.harnessRef.name value: kagent-tools-example @@ -84,66 +87,98 @@ tests: pattern: '\{\{include "builtin/skills-usage"\}\}' - lengthEqual: path: spec.template.inline.tools - count: 2 + count: 1 - equal: path: spec.template.inline.tools[0].mcp.server value: kind: RemoteMCPServer name: kagent-tool-server - equal: - path: spec.template.inline.tools[1].mcp.server + path: spec.template.inline.plugins[0].source value: - kind: RemoteMCPServer - name: kagent-tool-server + oci: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd + path: opt/kagent-tools-skills + + - it: should render the cluster-ops Agent + documentSelector: + path: metadata.name + value: kagent-tools-cluster-ops + asserts: + - equal: + path: spec.template.inline.plugins[0].skills + value: + - k8s-troubleshooting + - helm-release-management + - argo-rollouts-progressive-delivery + - prometheus-observability - lengthEqual: path: spec.template.inline.tools[0].mcp.tools - count: 33 - - lengthEqual: - path: spec.template.inline.tools[1].mcp.tools - count: 31 + count: 29 - contains: path: spec.template.inline.tools[0].mcp.tools content: helm_list_releases + + - it: should render the networking Agent + documentSelector: + path: metadata.name + value: kagent-tools-networking + asserts: - equal: - path: spec.template.inline.plugins + path: spec.template.inline.plugins[0].skills value: - - source: - oci: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd - path: opt/kagent-tools-skills - skills: - - k8s-troubleshooting - - helm-release-management - - istio-traffic-debugging - - argo-rollouts-progressive-delivery - - cilium-network-debugging - - kubescape-security-posture - - prometheus-observability + - istio-traffic-debugging + - cilium-network-debugging + - lengthEqual: + path: spec.template.inline.tools[0].mcp.tools + count: 27 + - contains: + path: spec.template.inline.tools[0].mcp.tools + content: istio_proxy_status + + - it: should render the security Agent + documentSelector: + path: metadata.name + value: kagent-tools-security + asserts: + - equal: + path: spec.template.inline.plugins[0].skills + value: + - kubescape-security-posture + - lengthEqual: + path: spec.template.inline.tools[0].mcp.tools + count: 10 + - contains: + path: spec.template.inline.tools[0].mcp.tools + content: kubescape_list_vulnerabilities - - it: should follow the release name, namespace override, and Agent name + - it: should follow the release name, namespace override, and name prefix release: name: custom set: namespaceOverride: agents - exampleAgent.name: tools-demo + exampleAgents.namePrefix: tools-demo asserts: - equal: path: metadata.namespace value: agents - equal: path: metadata.name - value: tools-demo + value: tools-demo-example + documentSelector: + path: kind + value: Harness - equal: path: spec.harnessRef.name - value: tools-demo + value: tools-demo-example documentSelector: - path: kind - value: Agent + path: metadata.name + value: tools-demo-security - equal: path: spec.template.inline.tools[0].mcp.server.name value: custom-tool-server documentSelector: - path: kind - value: Agent + path: metadata.name + value: tools-demo-networking - it: should mirror the skills and Harness images set: @@ -155,6 +190,7 @@ tests: documentSelector: path: kind value: Agent + matchMany: true - equal: path: spec.workload.image value: mirror.example/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -164,28 +200,28 @@ tests: - it: should fail without a skills image digest set: - exampleAgent.skillsImage: "" + exampleAgents.skillsImage: "" asserts: - failedTemplate: - errorMessage: "exampleAgent.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgent.skillsImage to @sha256:." + errorMessage: "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgents.skillsImage to @sha256:." - it: should fail when the skills image is not pinned by digest set: - exampleAgent.skillsImage: ghcr.io/kagent-dev/kagent/tools:0.3.0 + exampleAgents.skillsImage: ghcr.io/kagent-dev/kagent/tools:0.3.0 asserts: - failedTemplate: - errorMessage: 'exampleAgent.skillsImage must be pinned by sha256 digest: "ghcr.io/kagent-dev/kagent/tools:0.3.0"' + errorMessage: 'exampleAgents.skillsImage must be pinned by sha256 digest: "ghcr.io/kagent-dev/kagent/tools:0.3.0"' - it: should fail without the kagent-tools server set: kagent-tools.enabled: false asserts: - failedTemplate: - errorMessage: "exampleAgent.enabled requires kagent-tools.enabled: the example skills call the kagent-tools MCP server" + errorMessage: "exampleAgents.enabled requires kagent-tools.enabled: the example skills call the kagent-tools MCP server" - it: should fail without a snapshot location set: - exampleAgent.snapshotLocation: "" + exampleAgents.snapshotLocation: "" asserts: - failedTemplate: - errorMessage: "exampleAgent.snapshotLocation is required: set the snapshot storage location for the example Harness" + errorMessage: "exampleAgents.snapshotLocation is required: set the snapshot storage location for the example Harness" diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index e2503c7587..c5d8f4e1e7 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -25,7 +25,7 @@ global: # registry keys. Covers this chart's own images: controller, sandbox guest, ui, # substrateWorkerPool.workerImage, built-in Harness images # (both packaged defaults and config.harnessImages overrides), and the example - # Agent skills image (packaged default and exampleAgent.skillsImage). The vendored subcharts + # Agent skills image (packaged default and exampleAgents.skillsImage). The vendored subcharts # kagent-tools adopts it when its pinned version bumps. imageRegistry: "" # -- Pull secrets for every pod. The chart merges this list (union) into each @@ -772,16 +772,18 @@ kagent-tools: port: 8085 # Use a different port than the controller service port to avoid duplicate port definitions # ============================================================================== -# EXAMPLE AGENT +# EXAMPLE AGENTS # ============================================================================== -# One Agent with its own Go ADK Harness. It uses the default ModelConfig, the -# kagent-tools MCP server, and the seven skills packaged in the kagent-tools -# image under opt/kagent-tools-skills. Requires kagent-tools.enabled. -exampleAgent: - # -- Install the example Agent and its Harness. +# Three Agents (cluster-ops, networking, security) on one Go ADK Harness. They +# use the default ModelConfig, the kagent-tools MCP server, and the seven skills +# packaged in the kagent-tools image under opt/kagent-tools-skills. Requires +# kagent-tools.enabled. +exampleAgents: + # -- Install the example Agents and their Harness. enabled: false - # -- Name of the example Agent and its Harness. - name: kagent-tools-example + # -- Name prefix: the Harness is -example and the Agents are + # -cluster-ops, -networking, and -security. + namePrefix: kagent-tools # -- Digest-pinned kagent-tools image that holds the skills, for example # ghcr.io/kagent-dev/kagent/tools@sha256:. Empty uses the digest that # release packaging resolved for the kagent-tools version this chart pins. diff --git a/scripts/package-release-charts.sh b/scripts/package-release-charts.sh index 67c806d4d6..8d05d9d86b 100644 --- a/scripts/package-release-charts.sh +++ b/scripts/package-release-charts.sh @@ -18,8 +18,8 @@ bash "$root/scripts/harness-image-catalog.sh" "$version" "$metadata_dir" \ > "$staging/builtin-harness-images.json" # make tools-image-metadata records the kagent-tools image digest for the -# example Agent. Without it, the chart can enable that Agent only with an -# explicit exampleAgent.skillsImage. +# example Agents. Without it, the chart can enable those Agents only with an +# explicit exampleAgents.skillsImage. tools_metadata="$metadata_dir/kagent-tools.json" if [[ -e "$tools_metadata" ]]; then # The image tag must be the kagent-tools version the chart depends on, so a diff --git a/scripts/package-release-charts_test.sh b/scripts/package-release-charts_test.sh index ece888ee6c..959d273d35 100644 --- a/scripts/package-release-charts_test.sh +++ b/scripts/package-release-charts_test.sh @@ -24,7 +24,7 @@ for chart in kagent kagent-crds; do done cp "$root/helm/kagent/values.yaml" "$source_dir/kagent/values.yaml" cp "$root/helm/kagent/templates/"*.tpl "$root/helm/kagent/templates/controller-configmap.yaml" \ - "$root/helm/kagent/templates/example-agent.yaml" "$source_dir/kagent/templates/" + "$root/helm/kagent/templates/example-agents.yaml" "$source_dir/kagent/templates/" printf 'stale catalog\n' > "$source_dir/kagent/files/builtin-harness-images.json" printf 'stale tools image\n' > "$source_dir/kagent/files/kagent-tools-image.json" cp -R "$source_dir" "$scratch/source-before" @@ -52,8 +52,8 @@ render_images() { render_skills_source() { local archive="$1" shift - helm template release "$archive" --show-only templates/example-agent.yaml \ - --set exampleAgent.enabled=true --set exampleAgent.snapshotLocation=gs://bucket/kagent/ "$@" > "$scratch/rendered.yaml" + helm template release "$archive" --show-only templates/example-agents.yaml \ + --set exampleAgents.enabled=true --set exampleAgents.snapshotLocation=gs://bucket/kagent/ "$@" > "$scratch/rendered.yaml" yq -o=json 'select(.kind == "Agent") | .spec.template.inline.plugins[0].source' \ "$scratch/rendered.yaml" > "$scratch/rendered.json" } @@ -93,7 +93,7 @@ check="mirror and override the kagent-tools image" render_skills_source "$archive" --set global.imageRegistry=mirror.example/ jq -e --arg digest "$tools_digest" '.oci == "mirror.example/kagent-dev/kagent/tools@" + $digest' \ "$scratch/rendered.json" >/dev/null -render_skills_source "$archive" --set-string "exampleAgent.skillsImage=custom.example/team/tools@$config_digest" +render_skills_source "$archive" --set-string "exampleAgents.skillsImage=custom.example/team/tools@$config_digest" jq -e --arg config "$config_digest" '.oci == "custom.example/team/tools@" + $config' \ "$scratch/rendered.json" >/dev/null @@ -171,8 +171,8 @@ diff -r "$scratch/source-before" "$source_dir" check="release without kagent-tools metadata requires an explicit skills image" mv "$metadata_dir/kagent-tools.json" "$scratch/kagent-tools.json" release helm-release -if helm template release "$archive" --show-only templates/example-agent.yaml \ - --set exampleAgent.enabled=true --set exampleAgent.snapshotLocation=gs://bucket/kagent/ > "$scratch/rendered.yaml" 2> "$scratch/command.log"; then +if helm template release "$archive" --show-only templates/example-agents.yaml \ + --set exampleAgents.enabled=true --set exampleAgents.snapshotLocation=gs://bucket/kagent/ > "$scratch/rendered.yaml" 2> "$scratch/command.log"; then exit 1 fi [[ "$(< "$scratch/command.log")" == *'requires the kagent-tools image digest'* ]] diff --git a/scripts/tools-image-metadata.sh b/scripts/tools-image-metadata.sh index fee70c8786..73514418d3 100644 --- a/scripts/tools-image-metadata.sh +++ b/scripts/tools-image-metadata.sh @@ -14,7 +14,7 @@ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then exit 1 fi # kagent-tools 0.3.0 and earlier ship no opt/kagent-tools-skills folder. Record -# no digest for them, so the example Agent needs an explicit skillsImage. +# no digest for them, so the example Agents need an explicit skillsImage. last_without_skills="0.3.0" if [[ "$(printf '%s\n%s\n' "$version" "$last_without_skills" | sort -V | tail -n1)" == "$last_without_skills" ]]; then echo "kagent-tools $version has no skills folder; recording no digest" >&2 From a6c2bbf0a00a9686d8751f7aec3daea462229db6 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 14:12:49 -0400 Subject: [PATCH 03/12] docs(helm): drop the 0.x legacy list from the example Agents section Signed-off-by: Danielle Mustillo --- helm/README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/helm/README.md b/helm/README.md index c2adc69513..b929b00207 100644 --- a/helm/README.md +++ b/helm/README.md @@ -290,20 +290,6 @@ Notes: read-only, so the agent can still read the files of skills it did not select. -The example Agents replace the ready-made agents of kagent 0.x. These 0.x behaviors -are not carried forward: - -- one Helm flag per agent (for example `argo-rollouts-agent.enabled`); one - flag installs all three example Agents; -- the kgateway, cilium-policy, and promql agents, which have no matching skill; -- the Cilium install, upgrade, and ClusterMesh steps of the cilium-manager - agent; `cilium-network-debugging` covers only debugging; -- the persona text of each agent's system message; -- the Grafana tools of the observability agent (1.x has no Grafana MCP server), - and its call to the promql agent as a sub-agent; -- the `query_documentation` tool named in the helm, istio, and kgateway prompts; -- the A2A skill entries in `a2aConfig.skills`, which have no 1.x field. - ### Using Make ```bash From 294df08e880257d0721a949a551b4b06be86d660 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 16:49:25 -0400 Subject: [PATCH 04/12] test(e2e): cover all three example Agents in the skills e2e Read each example Agent's skills from the chart template and run every Agent shape on every harness. Each must advertise only its own skills and load one of them. Signed-off-by: Danielle Mustillo --- go/core/test/e2e/example_agent_skills_test.go | 185 +++++++++++------- .../mocks/invoke_example_agent_skills.json | 26 +-- 2 files changed, 129 insertions(+), 82 deletions(-) diff --git a/go/core/test/e2e/example_agent_skills_test.go b/go/core/test/e2e/example_agent_skills_test.go index 7699b0fac6..2c8e64588e 100644 --- a/go/core/test/e2e/example_agent_skills_test.go +++ b/go/core/test/e2e/example_agent_skills_test.go @@ -2,6 +2,8 @@ package e2e_test import ( "encoding/json" + "os" + "regexp" "slices" "strings" "testing" @@ -16,27 +18,57 @@ import ( kagentenv "github.com/kagent-dev/kagent/go/core/pkg/env" ) -// exampleAgentSkills are the kagent-tools skills that the chart's cluster-ops -// example Agent (helm/kagent/templates/example-agents.yaml) selects. -var exampleAgentSkills = []string{ - "k8s-troubleshooting", - "helm-release-management", - "argo-rollouts-progressive-delivery", - "prometheus-observability", +const exampleAgentsTemplate = "../../../../helm/kagent/templates/example-agents.yaml" + +// exampleAgentLoads names, for each example Agent in the chart, a question the +// mock model answers by loading one of the Agent's skills, and a tool that only +// that skill's body names. +var exampleAgentLoads = map[string]struct{ question, skill, tool string }{ + "cluster-ops": {"Which Helm releases are installed?", "helm-release-management", "helm_list_releases"}, + "networking": {"Why do requests fail inside the Istio mesh?", "istio-traffic-debugging", "istio_proxy_status"}, + "security": {"Is Kubescape healthy in this cluster?", "kubescape-security-posture", "kubescape_check_health"}, } -// exampleAgentOtherSkills are in the same image but belong to the other -// example Agents. -var exampleAgentOtherSkills = []string{ - "istio-traffic-debugging", - "cilium-network-debugging", - "kubescape-security-posture", +type exampleAgent struct { + name, description string + skills []string } -// TestExampleAgentToolsSkills runs the chart's cluster-ops example Agent -// behavior on every harness with a mock model: the kagent-tools image as an -// Agent Plugin, four of its skills, and the built-in prompt library. The chart's tool server -// binding is left out because the E2E install does not run kagent-tools. +var ( + exampleAgentPattern = regexp.MustCompile(`"name" "([^"]+)"\s+"description" "([^"]+)"\s+"skills" \(list ([^)]*)\)`) + quotedPattern = regexp.MustCompile(`"([^"]+)"`) +) + +// readExampleAgents reads each example Agent's name, description and skills +// from the chart template, so the test follows the chart. +func readExampleAgents(t *testing.T) []exampleAgent { + t.Helper() + data, err := os.ReadFile(exampleAgentsTemplate) + require.NoError(t, err) + var agents []exampleAgent + for _, match := range exampleAgentPattern.FindAllStringSubmatch(string(data), -1) { + agent := exampleAgent{name: match[1], description: match[2]} + for _, skill := range quotedPattern.FindAllStringSubmatch(match[3], -1) { + agent.skills = append(agent.skills, skill[1]) + } + agents = append(agents, agent) + } + names := make([]string, 0, len(agents)) + for _, agent := range agents { + names = append(names, agent.name) + } + want := make([]string, 0, len(exampleAgentLoads)) + for name := range exampleAgentLoads { + want = append(want, name) + } + require.ElementsMatch(t, want, names, "every example Agent in %s needs an entry in exampleAgentLoads", exampleAgentsTemplate) + return agents +} + +// TestExampleAgentToolsSkills runs each of the chart's example Agent shapes on +// every harness with a mock model: the kagent-tools image as an Agent Plugin, +// the Agent's own skills, and the built-in prompt library. The chart's tool +// server binding is left out because the E2E install does not run kagent-tools. func TestExampleAgentToolsSkills(t *testing.T) { t.Parallel() target := interactionTarget(t) @@ -44,65 +76,80 @@ func TestExampleAgentToolsSkills(t *testing.T) { if image == "" { t.Skip("KAGENT_E2E_TOOLS_SKILLS_IMAGE is not set; it needs a kagent-tools image with the opt/kagent-tools-skills folder") } - forEachHarness(t, func(t *testing.T, harness testHarness) { - t.Parallel() - model := startExampleAgentSkillsMock(t, harness.skillsRoot()) - kube := interactionKubeClient(t) - config := harness.createModel(t, kube, reachableModelURL(t, model.URL), nil) - template := &v1alpha3.AgentTemplate{ - ObjectMeta: metav1.ObjectMeta{GenerateName: "example-skills-", Namespace: "kagent", Labels: harness.labels()}, - Spec: v1alpha3.AgentTemplateSpec{ - ModelConfig: &corev1.LocalObjectReference{Name: config.Name}, - Description: "Kubernetes workload, Helm release, Argo Rollouts, and Prometheus metrics assistant.", - PromptTemplate: &v1alpha3.AgentTemplatePromptTemplateSpec{ - DataSources: []v1alpha3.AgentTemplatePromptSource{{Name: "kagent-builtin-prompts", Alias: "builtin"}}, - }, - SystemPrompt: "Answer questions about the Kubernetes cluster where kagent runs. When a\n" + - "question matches one of your skills, load that skill and follow it. Each\n" + - "skill names the kagent-tools MCP tools it uses.\n\n" + - "{{include \"builtin/skills-usage\"}}\n\n" + - "{{include \"builtin/kubernetes-context\"}}\n\n" + - "{{include \"builtin/tool-usage-best-practices\"}}\n\n" + - "{{include \"builtin/safety-guardrails\"}}\n", - Plugins: []v1alpha3.PluginBundle{{ - Source: v1alpha3.ArtifactSource{OCI: image, Path: "opt/kagent-tools-skills"}, - Skills: exampleAgentSkills, - }}, - }, + agents := readExampleAgents(t) + for _, agent := range agents { + load := exampleAgentLoads[agent.name] + require.Contains(t, agent.skills, load.skill, "%s must select the skill it loads", agent.name) + var others []string + for _, other := range agents { + if other.name != agent.name { + others = append(others, other.skills...) + } } - // Ready requires the runtime to mount the image and install the skills. - createAndWaitInteractionTemplate(t, harness, kube, template) - fixture := newInteractionFixtureForTemplate(t, harness, target, template.Name) - _, _, task := fixture.send(t, "Which Helm releases are installed?") - require.Equal(t, a2atype.TaskStateCompleted, task.Status.State, taskText(task)) - require.Contains(t, taskText(task), "EXAMPLE_SKILL_LOADED") + t.Run(agent.name, func(t *testing.T) { + t.Parallel() + forEachHarness(t, func(t *testing.T, harness testHarness) { + t.Parallel() + model := startExampleAgentSkillsMock(t, harness.skillsRoot(), load.question, load.skill, load.tool) + kube := interactionKubeClient(t) + config := harness.createModel(t, kube, reachableModelURL(t, model.URL), nil) + template := &v1alpha3.AgentTemplate{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "example-" + agent.name + "-", Namespace: "kagent", Labels: harness.labels()}, + Spec: v1alpha3.AgentTemplateSpec{ + ModelConfig: &corev1.LocalObjectReference{Name: config.Name}, + Description: agent.description, + PromptTemplate: &v1alpha3.AgentTemplatePromptTemplateSpec{ + DataSources: []v1alpha3.AgentTemplatePromptSource{{Name: "kagent-builtin-prompts", Alias: "builtin"}}, + }, + SystemPrompt: "Answer questions about the Kubernetes cluster where kagent runs. When a\n" + + "question matches one of your skills, load that skill and follow it. Each\n" + + "skill names the kagent-tools MCP tools it uses.\n\n" + + "{{include \"builtin/skills-usage\"}}\n\n" + + "{{include \"builtin/kubernetes-context\"}}\n\n" + + "{{include \"builtin/tool-usage-best-practices\"}}\n\n" + + "{{include \"builtin/safety-guardrails\"}}\n", + Plugins: []v1alpha3.PluginBundle{{ + Source: v1alpha3.ArtifactSource{OCI: image, Path: "opt/kagent-tools-skills"}, + Skills: agent.skills, + }}, + }, + } + // Ready requires the runtime to mount the image and install the skills. + createAndWaitInteractionTemplate(t, harness, kube, template) + fixture := newInteractionFixtureForTemplate(t, harness, target, template.Name) + _, _, task := fixture.send(t, load.question) + require.Equal(t, a2atype.TaskStateCompleted, task.Status.State, taskText(task)) + require.Contains(t, taskText(task), "EXAMPLE_SKILL_LOADED") - // Claude also sends requests without a body; only model turns count. - requests := slices.DeleteFunc(model.Requests("", ""), func(r recordedModelRequest) bool { return len(r.Body) == 0 }) - require.GreaterOrEqual(t, len(requests), 2, "the model must be called again after loading the skill") - first := string(requests[0].Body) - for _, skill := range exampleAgentSkills { - require.Contains(t, first, skill, "skill must be advertised to the model") - } - for _, skill := range exampleAgentOtherSkills { - require.NotContains(t, first, skill, "unselected skill must not be advertised") - } - require.Contains(t, first, "## Safety Guidelines", "built-in prompts must be included") - // Discovery exposes metadata only. The skill body names its MCP tools. - require.NotContains(t, first, "helm_list_releases") - loaded := false - for _, request := range requests[1:] { - loaded = loaded || strings.Contains(string(request.Body), "helm_list_releases") - } - require.True(t, loaded, "the model did not receive the helm-release-management skill body") - }) + // Claude also sends requests without a body; only model turns count. + requests := slices.DeleteFunc(model.Requests("", ""), func(r recordedModelRequest) bool { return len(r.Body) == 0 }) + require.GreaterOrEqual(t, len(requests), 2, "the model must be called again after loading the skill") + first := string(requests[0].Body) + for _, skill := range agent.skills { + require.Contains(t, first, skill, "skill must be advertised to the model") + } + for _, skill := range others { + require.NotContains(t, first, skill, "another example Agent's skill must not be advertised") + } + require.Contains(t, first, "## Safety Guidelines", "built-in prompts must be included") + // Discovery exposes metadata only. The skill body names its MCP tools. + require.NotContains(t, first, load.tool) + loaded := false + for _, request := range requests[1:] { + loaded = loaded || strings.Contains(string(request.Body), load.tool) + } + require.True(t, loaded, "the model did not receive the %s skill body", load.skill) + }) + }) + } } -func startExampleAgentSkillsMock(t *testing.T, root string) *modelRecorder { +func startExampleAgentSkillsMock(t *testing.T, root, question, skill, tool string) *modelRecorder { t.Helper() fixture, err := interactionMocks.ReadFile("mocks/invoke_example_agent_skills.json") require.NoError(t, err) + replacer := strings.NewReplacer("ROOT", root, "QUESTION", question, "SKILL_NAME", skill, "SKILL_TOOL", tool) var config mockllm.Config - require.NoError(t, json.Unmarshal([]byte(strings.ReplaceAll(string(fixture), "ROOT", root)), &config)) + require.NoError(t, json.Unmarshal([]byte(replacer.Replace(string(fixture))), &config)) return startModelRecorder(t, mockLLMProxy(t, startMockLLMConfig(t, config)), nil) } diff --git a/go/core/test/e2e/mocks/invoke_example_agent_skills.json b/go/core/test/e2e/mocks/invoke_example_agent_skills.json index 5cc030eb92..3dd97ba41b 100644 --- a/go/core/test/e2e/mocks/invoke_example_agent_skills.json +++ b/go/core/test/e2e/mocks/invoke_example_agent_skills.json @@ -6,7 +6,7 @@ "match_type": "contains", "message": { "role": "user", - "content": "Which Helm releases are installed?" + "content": "QUESTION" } }, "response": { @@ -22,11 +22,11 @@ "content": "", "tool_calls": [ { - "id": "call_load_helm_release_management", + "id": "call_load_skill", "type": "function", "function": { "name": "load_skill", - "arguments": "{\"name\": \"helm-release-management\"}" + "arguments": "{\"name\": \"SKILL_NAME\"}" } } ] @@ -42,7 +42,7 @@ "match_type": "contains", "message": { "role": "tool", - "content": "helm_list_releases" + "content": "SKILL_TOOL" } }, "response": { @@ -68,7 +68,7 @@ "name": "example_skills_load", "match": { "match_type": "contains", - "input": "Which Helm releases are installed?" + "input": "QUESTION" }, "response": { "id": "example_skills_load", @@ -77,12 +77,12 @@ "model": "gpt-5.2-codex", "output": [ { - "id": "fc_load_helm_release_management", + "id": "fc_load_skill", "type": "function_call", "status": "completed", - "call_id": "call_load_helm_release_management", + "call_id": "call_load_skill", "name": "exec_command", - "arguments": "{\"cmd\": \"cat ROOT/helm-release-management/SKILL.md\"}" + "arguments": "{\"cmd\": \"cat ROOT/SKILL_NAME/SKILL.md\"}" } ], "parallel_tool_calls": false, @@ -98,7 +98,7 @@ "name": "example_skills_loaded", "match": { "match_type": "contains", - "input": "helm_list_releases" + "input": "SKILL_TOOL" }, "response": { "id": "example_skills_loaded", @@ -140,7 +140,7 @@ "content": [ { "type": "text", - "text": "Which Helm releases are installed?" + "text": "QUESTION" } ] } @@ -152,10 +152,10 @@ "content": [ { "type": "tool_use", - "id": "toolu_load_helm_release_management", + "id": "toolu_load_skill", "name": "Skill", "input": { - "skill": "helm-release-management" + "skill": "SKILL_NAME" } } ], @@ -176,7 +176,7 @@ "content": [ { "type": "text", - "text": "helm_list_releases" + "text": "SKILL_TOOL" } ] } From e8b8dbe07cd5a958939663f13a034b243dae250a Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 17:12:54 -0400 Subject: [PATCH 05/12] fix(helm): use the release default image in the example Harness The example Harness copied the release's Go ADK image into spec.workload.image, which marks it as an explicit override that no longer follows the image catalog. Leave workload unset so the controller selects the release default. Give the networking Agent the Kubernetes read tools its Istio and Cilium skills need to find pods, services, logs, and events. Update the e2e README for the three-Agent test. Signed-off-by: Danielle Mustillo --- go/core/test/e2e/README.md | 12 +++++----- helm/README.md | 2 +- helm/kagent/templates/example-agents.yaml | 8 ++----- helm/kagent/tests/example-agents_test.yaml | 28 ++++------------------ 4 files changed, 14 insertions(+), 36 deletions(-) diff --git a/go/core/test/e2e/README.md b/go/core/test/e2e/README.md index 397263a2bb..fcd591c52f 100644 --- a/go/core/test/e2e/README.md +++ b/go/core/test/e2e/README.md @@ -7,14 +7,14 @@ the three `config.harnessImages` defaults matching the explicit Harness fixtures. The test checks runtime readiness, effective-image status, and that the controller leaves the user spec unchanged. -`TestExampleAgentToolsSkills` runs the chart's cluster-ops example Agent -behavior on every harness with a mock model. It needs +`TestExampleAgentToolsSkills` runs each of the chart's three example Agents +on every harness with a mock model. It reads each Agent's name, description, +and skills from `helm/kagent/templates/example-agents.yaml`. It needs `KAGENT_E2E_TOOLS_SKILLS_IMAGE`, a digest-pinned kagent-tools image that contains the `opt/kagent-tools-skills` Agent Plugin, in the local test registry; -the test is skipped without it. It checks that the Agent's four skills are -advertised, that the other three are not, and that `helm-release-management` -loads. It omits the tool server binding because the test install does not run -kagent-tools. +the test is skipped without it. For each Agent it checks that exactly the +Agent's own skills are advertised and that one of them loads. It omits the tool +server binding because the test install does not run kagent-tools. The standalone sandbox suite needs a Substrate WorkerPool with available capacity. Set `config.sandboxes.guestImage` to the diff --git a/helm/README.md b/helm/README.md index b929b00207..8cc1bad470 100644 --- a/helm/README.md +++ b/helm/README.md @@ -245,7 +245,7 @@ install three example Agents. They are off by default. The chart then creates: | Agent | Skills | Tools | |---|---|---| | `-cluster-ops` | `k8s-troubleshooting`, `helm-release-management`, `argo-rollouts-progressive-delivery`, `prometheus-observability` | 29 | - | `-networking` | `istio-traffic-debugging`, `cilium-network-debugging` | 27 | + | `-networking` | `istio-traffic-debugging`, `cilium-network-debugging` | 31 | | `-security` | `kubescape-security-posture` | 10 | Each Agent uses `default-model-config` and includes the `skills-usage`, diff --git a/helm/kagent/templates/example-agents.yaml b/helm/kagent/templates/example-agents.yaml index e0b9ee3815..ce6659f080 100644 --- a/helm/kagent/templates/example-agents.yaml +++ b/helm/kagent/templates/example-agents.yaml @@ -6,7 +6,6 @@ {{- fail "exampleAgents.snapshotLocation is required: set the snapshot storage location for the example Harness" }} {{- end }} {{- $skillsImage := include "kagent.exampleAgentsSkillsImage" . }} -{{- $harnessImage := get (include "kagent.builtinHarnessImages" . | fromJson) "kagent" }} {{- $prefix := .Values.exampleAgents.namePrefix }} {{- $toolServer := printf "%s-tool-server" (include "kagent.fullname" .) }} {{- /* @@ -33,7 +32,8 @@ OpenAI and Azure OpenAI accept at most 128 tools in one request. "description" "Istio service mesh and Cilium networking assistant." "skills" (list "istio-traffic-debugging" "cilium-network-debugging") "tools" (list - "k8s_apply_manifest" "k8s_get_resource_yaml" + "k8s_apply_manifest" "k8s_describe_resource" "k8s_get_events" "k8s_get_pod_logs" "k8s_get_resource_yaml" + "k8s_get_resources" "istio_analyze_cluster_configuration" "istio_apply_waypoint" "istio_delete_waypoint" "istio_list_waypoints" "istio_proxy_config" "istio_proxy_status" "istio_version" "istio_waypoint_status" "istio_ztunnel_config" "cilium_display_policy_node_information" "cilium_display_selectors" "cilium_flush_ipsec_state" @@ -62,10 +62,6 @@ metadata: {{- include "kagent.labels" . | nindent 4 }} spec: kagent: {} - {{- with $harnessImage }} - workload: - image: {{ . | quote }} - {{- end }} substrate: workerPoolRef: name: {{ .Values.substrateWorkerPool.name | quote }} diff --git a/helm/kagent/tests/example-agents_test.yaml b/helm/kagent/tests/example-agents_test.yaml index 8f8ca2cac6..967ab59b98 100644 --- a/helm/kagent/tests/example-agents_test.yaml +++ b/helm/kagent/tests/example-agents_test.yaml @@ -8,7 +8,6 @@ set: exampleAgents.enabled: true exampleAgents.snapshotLocation: gs://bucket/kagent/ exampleAgents.skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd - config.harnessImages.kagent: ghcr.io/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa tests: - it: should render nothing by default set: @@ -22,7 +21,7 @@ tests: - hasDocuments: count: 4 - - it: should render a Go ADK Harness with the release image + - it: should render a Go ADK Harness that uses the release default image documentSelector: path: kind value: Harness @@ -39,9 +38,8 @@ tests: - equal: path: spec.kagent value: {} - - equal: - path: spec.workload.image - value: ghcr.io/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + - notExists: + path: spec.workload - equal: path: spec.substrate.workerPoolRef.name value: kagent-default @@ -49,16 +47,6 @@ tests: path: spec.substrate.snapshotPolicy.location value: gs://bucket/kagent/ - - it: should omit the workload when the release has no Go ADK image - set: - config.harnessImages.kagent: "" - documentSelector: - path: kind - value: Harness - asserts: - - notExists: - path: spec.workload - - it: should give every Agent the Harness, model, prompts, skills image, and one tool server binding documentSelector: path: kind @@ -130,7 +118,7 @@ tests: - cilium-network-debugging - lengthEqual: path: spec.template.inline.tools[0].mcp.tools - count: 27 + count: 31 - contains: path: spec.template.inline.tools[0].mcp.tools content: istio_proxy_status @@ -180,7 +168,7 @@ tests: path: metadata.name value: tools-demo-networking - - it: should mirror the skills and Harness images + - it: should mirror the skills image set: global.imageRegistry: mirror.example/ asserts: @@ -191,12 +179,6 @@ tests: path: kind value: Agent matchMany: true - - equal: - path: spec.workload.image - value: mirror.example/kagent-dev/kagent/golang-adk@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - documentSelector: - path: kind - value: Harness - it: should fail without a skills image digest set: From 829fedfa39a63d3559cff3015b01aab546046b37 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 21:49:15 -0400 Subject: [PATCH 06/12] chore(release): drop the guard for a kagent-tools image file nothing writes Only release packaging writes kagent-tools-image.json, and it writes it into its staging folder, never into the chart sources. Signed-off-by: Danielle Mustillo --- .gitignore | 1 - scripts/package-release-charts.sh | 1 - scripts/package-release-charts_test.sh | 3 --- 3 files changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index 957413efde..849b205b75 100644 --- a/.gitignore +++ b/.gitignore @@ -209,7 +209,6 @@ file::memory:* ## Helm /helm/kagent/files/builtin-harness-images.json -/helm/kagent/files/kagent-tools-image.json *.tgz /tmp/ /reports/*.csv diff --git a/scripts/package-release-charts.sh b/scripts/package-release-charts.sh index 8d05d9d86b..fbee84ac41 100644 --- a/scripts/package-release-charts.sh +++ b/scripts/package-release-charts.sh @@ -41,7 +41,6 @@ fi cp -R "$source_dir" "$staging/helm" mkdir -p "$staging/helm/kagent/files" mv "$staging/builtin-harness-images.json" "$staging/helm/kagent/files/builtin-harness-images.json" -rm -f "$staging/helm/kagent/files/kagent-tools-image.json" if [[ -f "$staging/kagent-tools-image.json" ]]; then mv "$staging/kagent-tools-image.json" "$staging/helm/kagent/files/kagent-tools-image.json" fi diff --git a/scripts/package-release-charts_test.sh b/scripts/package-release-charts_test.sh index 959d273d35..766658fc4d 100644 --- a/scripts/package-release-charts_test.sh +++ b/scripts/package-release-charts_test.sh @@ -26,7 +26,6 @@ cp "$root/helm/kagent/values.yaml" "$source_dir/kagent/values.yaml" cp "$root/helm/kagent/templates/"*.tpl "$root/helm/kagent/templates/controller-configmap.yaml" \ "$root/helm/kagent/templates/example-agents.yaml" "$source_dir/kagent/templates/" printf 'stale catalog\n' > "$source_dir/kagent/files/builtin-harness-images.json" -printf 'stale tools image\n' > "$source_dir/kagent/files/kagent-tools-image.json" cp -R "$source_dir" "$scratch/source-before" for image in golang-adk claude-harness codex-harness; do jq -n --arg digest "$digest" --arg config "$config_digest" \ @@ -166,8 +165,6 @@ expect_failure kagent/Chart-template.yaml helm-release mv "$scratch/Chart-template.yaml" "$source_dir/kagent/Chart-template.yaml" diff -r "$scratch/source-before" "$source_dir" -# The stale source file would fail to parse, so the message also proves that -# packaging dropped it. check="release without kagent-tools metadata requires an explicit skills image" mv "$metadata_dir/kagent-tools.json" "$scratch/kagent-tools.json" release helm-release From 6dd45b06b5aa875925a7b15adda6ecad49519cec Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 21:49:44 -0400 Subject: [PATCH 07/12] refactor(helm): inline the example Agents skills image helper It had one caller. The example Agents' setup checks now sit together at the top of example-agents.yaml. Signed-off-by: Danielle Mustillo --- helm/kagent/templates/_helpers.tpl | 20 -------------------- helm/kagent/templates/example-agents.yaml | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/helm/kagent/templates/_helpers.tpl b/helm/kagent/templates/_helpers.tpl index 724c2b8156..8774839610 100644 --- a/helm/kagent/templates/_helpers.tpl +++ b/helm/kagent/templates/_helpers.tpl @@ -13,26 +13,6 @@ {{- $images | mustToJson -}} {{- end -}} -{{/* -Digest-pinned kagent-tools image whose skills the example Agents mount. Release -packaging resolves the digest of the kagent-tools version this chart pins. -*/}} -{{- define "kagent.exampleAgentsSkillsImage" -}} -{{- $image := .Values.exampleAgents.skillsImage -}} -{{- if not $image -}} - {{- with .Files.Get "files/kagent-tools-image.json" -}} - {{- $image = get (mustFromJson .) "image" -}} - {{- end -}} -{{- end -}} -{{- if not $image -}} - {{- fail "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgents.skillsImage to @sha256:." -}} -{{- end -}} -{{- if not (regexMatch "^[^[:space:]@]+@sha256:[0-9a-f]{64}$" $image) -}} - {{- fail (printf "exampleAgents.skillsImage must be pinned by sha256 digest: %q" $image) -}} -{{- end -}} -{{- include "kagent.mirroredImage" (dict "root" . "image" $image) -}} -{{- end -}} - {{/* Create a default fully qualified app name. */}} diff --git a/helm/kagent/templates/example-agents.yaml b/helm/kagent/templates/example-agents.yaml index ce6659f080..9cba08cc23 100644 --- a/helm/kagent/templates/example-agents.yaml +++ b/helm/kagent/templates/example-agents.yaml @@ -5,7 +5,20 @@ {{- if not .Values.exampleAgents.snapshotLocation }} {{- fail "exampleAgents.snapshotLocation is required: set the snapshot storage location for the example Harness" }} {{- end }} -{{- $skillsImage := include "kagent.exampleAgentsSkillsImage" . }} +{{- /* Release packaging resolves the digest of the kagent-tools version this chart pins. */}} +{{- $skillsImage := .Values.exampleAgents.skillsImage }} +{{- if not $skillsImage }} +{{- with .Files.Get "files/kagent-tools-image.json" }} +{{- $skillsImage = get (mustFromJson .) "image" }} +{{- end }} +{{- end }} +{{- if not $skillsImage }} +{{- fail "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgents.skillsImage to @sha256:." }} +{{- end }} +{{- if not (regexMatch "^[^[:space:]@]+@sha256:[0-9a-f]{64}$" $skillsImage) }} +{{- fail (printf "exampleAgents.skillsImage must be pinned by sha256 digest: %q" $skillsImage) }} +{{- end }} +{{- $skillsImage = include "kagent.mirroredImage" (dict "root" . "image" $skillsImage) }} {{- $prefix := .Values.exampleAgents.namePrefix }} {{- $toolServer := printf "%s-tool-server" (include "kagent.fullname" .) }} {{- /* From 4d31cb6cb19eeb967ba8bfd441bf70d871fb601a Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 21:51:00 -0400 Subject: [PATCH 08/12] refactor(release): write kagent-tools metadata in its final shape tools-image-metadata.sh now writes {image, version} to the output file itself, and removes an old file first. Release packaging checks the version against the chart's kagent-tools dependency and copies the file, instead of parsing a Buildx-shaped record back into an image reference. The Makefile recipe no longer juggles a temp file. Signed-off-by: Danielle Mustillo --- Makefile | 5 +---- scripts/package-release-charts.sh | 17 ++++++----------- scripts/package-release-charts_test.sh | 14 +++++++------- scripts/tools-image-metadata.sh | 14 ++++++++------ 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index a62b9dfef4..60962a4ac7 100644 --- a/Makefile +++ b/Makefile @@ -625,10 +625,7 @@ helm-release: ## Package release charts from RUNTIME_IMAGE_METADATA_DIR without .PHONY: tools-image-metadata tools-image-metadata: ## Record the digest of the kagent-tools image the chart pins in RUNTIME_IMAGE_METADATA_DIR @test -n "$(RUNTIME_IMAGE_METADATA_DIR)" || { echo "RUNTIME_IMAGE_METADATA_DIR is required for release chart packaging" >&2; exit 1; } - mkdir -p "$(RUNTIME_IMAGE_METADATA_DIR)" - rm -f "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json" "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" - bash scripts/tools-image-metadata.sh "$(HELM_SOURCE_DIR)/kagent/Chart-template.yaml" "$(TOOLS_IMAGE_REPOSITORY)" > "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" || { rm -f "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp"; exit 1; } - if [ -s "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" ]; then mv "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp" "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json"; else rm -f "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json.tmp"; fi + bash scripts/tools-image-metadata.sh "$(HELM_SOURCE_DIR)/kagent/Chart-template.yaml" "$(TOOLS_IMAGE_REPOSITORY)" "$(RUNTIME_IMAGE_METADATA_DIR)/kagent-tools.json" .PHONY: helm-publish helm-publish: ## Package and push all Helm charts to the OCI registry diff --git a/scripts/package-release-charts.sh b/scripts/package-release-charts.sh index fbee84ac41..45efa07c1e 100644 --- a/scripts/package-release-charts.sh +++ b/scripts/package-release-charts.sh @@ -22,18 +22,13 @@ bash "$root/scripts/harness-image-catalog.sh" "$version" "$metadata_dir" \ # explicit exampleAgents.skillsImage. tools_metadata="$metadata_dir/kagent-tools.json" if [[ -e "$tools_metadata" ]]; then - # The image tag must be the kagent-tools version the chart depends on, so a + # The version must be the kagent-tools version the chart depends on, so a # stale metadata file cannot pin another release's skills. tools_version="$(yq '(.dependencies // [])[] | select(.name == "kagent-tools") | .version' "$source_dir/kagent/Chart-template.yaml")" jq -e --arg tools_version "$tools_version" ' - .["containerimage.digest"] as $digest | .["image.name"] as $name | - if ($digest | type) == "string" and ($digest | test("^sha256:[a-f0-9]{64}$")) and - ($name | type) == "string" and $name != "" and - ($tools_version == "" or ($name | split("@")[0] | capture(":(?[^/:]+)$").t) == $tools_version) - then {image: (($name | split("@")[0] | sub(":[^/:]+$"; "")) + "@" + $digest)} - else error("missing or invalid final image digest for kagent-tools") - end' "$tools_metadata" > "$staging/kagent-tools-image.json" || { - echo "Invalid kagent-tools image metadata, or its tag is not kagent-tools $tools_version: $tools_metadata. Run make tools-image-metadata." >&2 + (.image | type) == "string" and (.image | test("^[^[:space:]@]+@sha256:[a-f0-9]{64}$")) and + ($tools_version == "" or .version == $tools_version)' "$tools_metadata" >/dev/null || { + echo "Invalid kagent-tools image metadata, or it is not for kagent-tools $tools_version: $tools_metadata. Run make tools-image-metadata." >&2 exit 1 } fi @@ -41,8 +36,8 @@ fi cp -R "$source_dir" "$staging/helm" mkdir -p "$staging/helm/kagent/files" mv "$staging/builtin-harness-images.json" "$staging/helm/kagent/files/builtin-harness-images.json" -if [[ -f "$staging/kagent-tools-image.json" ]]; then - mv "$staging/kagent-tools-image.json" "$staging/helm/kagent/files/kagent-tools-image.json" +if [[ -e "$tools_metadata" ]]; then + cp "$tools_metadata" "$staging/helm/kagent/files/kagent-tools-image.json" fi # Reuse local chart preparation, keeping generated files and intermediate archives isolated. diff --git a/scripts/package-release-charts_test.sh b/scripts/package-release-charts_test.sh index 766658fc4d..9031ec0ec4 100644 --- a/scripts/package-release-charts_test.sh +++ b/scripts/package-release-charts_test.sh @@ -32,7 +32,7 @@ for image in golang-adk claude-harness codex-harness; do '{"containerimage.digest": $digest, "containerimage.config.digest": $config}' > "$metadata_dir/$image.json" done jq -n --arg digest "$tools_digest" \ - '{"image.name": "ghcr.io/kagent-dev/kagent/tools:0.3.0", "containerimage.digest": $digest}' > "$metadata_dir/kagent-tools.json" + '{image: ("ghcr.io/kagent-dev/kagent/tools@" + $digest), version: "0.3.0"}' > "$metadata_dir/kagent-tools.json" release() { make --no-print-directory -C "$root" VERSION="$version" AWK=awk \ @@ -145,8 +145,8 @@ expect_failure codex-harness helm-release cp "$scratch/codex-valid.json" "$metadata_dir/codex-harness.json" cp "$metadata_dir/kagent-tools.json" "$scratch/kagent-tools-valid.json" -for invalid in '{}' '{"image.name":"ghcr.io/kagent-dev/kagent/tools:0.3.0","containerimage.digest":"sha256:bad"}' \ - '{"containerimage.digest":"'"$tools_digest"'"}' '{broken'; do +for invalid in '{}' '{"image":"ghcr.io/kagent-dev/kagent/tools@sha256:bad","version":"0.3.0"}' \ + '{"image":"ghcr.io/kagent-dev/kagent/tools:0.3.0","version":"0.3.0"}' '{broken'; do check="invalid kagent-tools metadata: $invalid" printf '%s\n' "$invalid" > "$metadata_dir/kagent-tools.json" expect_failure kagent-tools helm-release @@ -156,7 +156,7 @@ cp "$scratch/kagent-tools-valid.json" "$metadata_dir/kagent-tools.json" check="kagent-tools metadata for another kagent-tools version" cp "$source_dir/kagent/Chart-template.yaml" "$scratch/kagent-chart-template.yaml" printf 'dependencies:\n - name: kagent-tools\n version: 0.3.1\n' >> "$source_dir/kagent/Chart-template.yaml" -expect_failure 'its tag is not kagent-tools 0.3.1' helm-release +expect_failure 'it is not for kagent-tools 0.3.1' helm-release cp "$scratch/kagent-chart-template.yaml" "$source_dir/kagent/Chart-template.yaml" check="packaging failure preserves previous archives" @@ -197,7 +197,7 @@ check="tools-image-metadata resolves the pinned kagent-tools version" TOOLS_FIXTURE_DIGEST="$tools_digest" resolve_tools TOOLS_IMAGE_REPOSITORY=registry.example/team/tools grep -qx 'docker buildx imagetools inspect registry.example/team/tools:0.3.1' "$scratch/tools.log" jq -e --arg digest "$tools_digest" \ - '. == {"image.name": "registry.example/team/tools:0.3.1", "containerimage.digest": $digest}' \ + '. == {image: ("registry.example/team/tools@" + $digest), version: "0.3.1"}' \ "$scratch/tools-metadata/kagent-tools.json" >/dev/null check="tools-image-metadata records no digest for kagent-tools without skills" @@ -211,8 +211,8 @@ printf 'apiVersion: v2\nname: kagent\nversion: ${VERSION}\ndependencies:\n - na 0.3.1 > "$scratch/tools-source/kagent/Chart-template.yaml" TOOLS_FIXTURE_DIGEST="$tools_digest" resolve_tools -check="tools-image-metadata requires a resolved digest" -rm "$scratch/tools-metadata/kagent-tools.json" +check="tools-image-metadata requires a resolved digest and drops the old file" +test -e "$scratch/tools-metadata/kagent-tools.json" if TOOLS_FIXTURE_DIGEST= resolve_tools; then exit 1 fi diff --git a/scripts/tools-image-metadata.sh b/scripts/tools-image-metadata.sh index 73514418d3..089c484f46 100644 --- a/scripts/tools-image-metadata.sh +++ b/scripts/tools-image-metadata.sh @@ -3,11 +3,13 @@ set -euo pipefail # Called by make tools-image-metadata. kagent-tools is released from its own # repository, so no build here records its digest; resolve it from the registry. -# The output uses the Buildx metadata keys that release packaging validates. chart_template="${1:?kagent Chart-template.yaml is required}" repository="${2:?kagent-tools image repository is required}" +output="${3:?output file is required}" command -v yq >/dev/null || { echo "tools-image-metadata.sh needs yq (mikefarah yq v4)" >&2; exit 1; } +mkdir -p "$(dirname "$output")" +rm -f "$output" version="$(yq -e '.dependencies[] | select(.name == "kagent-tools") | .version' "$chart_template")" if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "The kagent-tools dependency in $chart_template must pin an exact version, got: $version" >&2 @@ -25,8 +27,8 @@ image="$repository:$version" # Read all output so Docker does not get a broken pipe under pipefail. digest="$(docker buildx imagetools inspect "$image" | awk '$1 == "Digest:" { print $2 }')" -jq -en --arg image "$image" --arg digest "$digest" ' - if $digest | test("^sha256:[a-f0-9]{64}$") - then {"image.name": $image, "containerimage.digest": $digest} - else error("could not resolve the digest of " + $image) - end' +if [[ ! "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]; then + echo "could not resolve the digest of $image" >&2 + exit 1 +fi +jq -n --arg image "$repository@$digest" --arg version "$version" '{image: $image, version: $version}' > "$output" From 8ba54e1a2f26f799b76c7da4a0e267fd27ce9f42 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 22:28:07 -0400 Subject: [PATCH 09/12] fix(e2e): use v1alpha3.ObjectReference for the example Agents ModelConfig Signed-off-by: Danielle Mustillo --- go/core/test/e2e/example_agent_skills_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go/core/test/e2e/example_agent_skills_test.go b/go/core/test/e2e/example_agent_skills_test.go index 2c8e64588e..d10e5f2923 100644 --- a/go/core/test/e2e/example_agent_skills_test.go +++ b/go/core/test/e2e/example_agent_skills_test.go @@ -11,7 +11,6 @@ import ( a2atype "github.com/a2aproject/a2a-go/v2/a2a" "github.com/kagent-dev/mockllm" "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kagent-dev/kagent/go/api/v1alpha3" @@ -96,7 +95,7 @@ func TestExampleAgentToolsSkills(t *testing.T) { template := &v1alpha3.AgentTemplate{ ObjectMeta: metav1.ObjectMeta{GenerateName: "example-" + agent.name + "-", Namespace: "kagent", Labels: harness.labels()}, Spec: v1alpha3.AgentTemplateSpec{ - ModelConfig: &corev1.LocalObjectReference{Name: config.Name}, + ModelConfig: &v1alpha3.ObjectReference{Name: config.Name}, Description: agent.description, PromptTemplate: &v1alpha3.AgentTemplatePromptTemplateSpec{ DataSources: []v1alpha3.AgentTemplatePromptSource{{Name: "kagent-builtin-prompts", Alias: "builtin"}}, From b517acc9e39eab4a335298a5859e2b1c85d6d515 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 22:58:56 -0400 Subject: [PATCH 10/12] refactor(helm): write out each example Agent in full The template now shows the Harness and the three Agents as plain YAML, instead of a dictionary of names, skills and tools fed to a range loop, so a reader sees what the chart deploys. The rendered output is unchanged. The skills e2e test reads each Agent from the YAML lines. Signed-off-by: Danielle Mustillo --- go/core/test/e2e/example_agent_skills_test.go | 20 +- helm/kagent/templates/example-agents.yaml | 217 ++++++++++++++---- 2 files changed, 182 insertions(+), 55 deletions(-) diff --git a/go/core/test/e2e/example_agent_skills_test.go b/go/core/test/e2e/example_agent_skills_test.go index d10e5f2923..7d299694e4 100644 --- a/go/core/test/e2e/example_agent_skills_test.go +++ b/go/core/test/e2e/example_agent_skills_test.go @@ -34,8 +34,9 @@ type exampleAgent struct { } var ( - exampleAgentPattern = regexp.MustCompile(`"name" "([^"]+)"\s+"description" "([^"]+)"\s+"skills" \(list ([^)]*)\)`) - quotedPattern = regexp.MustCompile(`"([^"]+)"`) + exampleAgentNamePattern = regexp.MustCompile(`(?m)^ name: \{\{ printf "%s-([a-z-]+)" \$prefix`) + exampleAgentDescriptionPattern = regexp.MustCompile(`(?m)^ description: "([^"]+)"$`) + exampleAgentSkillsPattern = regexp.MustCompile(`(?m)^ skills:\n((?: - \S+(?:\n|\z))+)`) ) // readExampleAgents reads each example Agent's name, description and skills @@ -45,10 +46,17 @@ func readExampleAgents(t *testing.T) []exampleAgent { data, err := os.ReadFile(exampleAgentsTemplate) require.NoError(t, err) var agents []exampleAgent - for _, match := range exampleAgentPattern.FindAllStringSubmatch(string(data), -1) { - agent := exampleAgent{name: match[1], description: match[2]} - for _, skill := range quotedPattern.FindAllStringSubmatch(match[3], -1) { - agent.skills = append(agent.skills, skill[1]) + for doc := range strings.SplitSeq(string(data), "\n---\n") { + if !strings.Contains(doc, "\nkind: Agent\n") { + continue + } + name := exampleAgentNamePattern.FindStringSubmatch(doc) + description := exampleAgentDescriptionPattern.FindStringSubmatch(doc) + skills := exampleAgentSkillsPattern.FindStringSubmatch(doc) + require.True(t, name != nil && description != nil && skills != nil, "cannot read an example Agent in %s:\n%s", exampleAgentsTemplate, doc) + agent := exampleAgent{name: name[1], description: description[1]} + for line := range strings.SplitSeq(strings.TrimSpace(skills[1]), "\n") { + agent.skills = append(agent.skills, strings.TrimPrefix(strings.TrimSpace(line), "- ")) } agents = append(agents, agent) } diff --git a/helm/kagent/templates/example-agents.yaml b/helm/kagent/templates/example-agents.yaml index 9cba08cc23..625f531f3e 100644 --- a/helm/kagent/templates/example-agents.yaml +++ b/helm/kagent/templates/example-agents.yaml @@ -26,46 +26,6 @@ Each Agent binds only the tools its skills name, in one binding. Claude and Codex accept each MCP server once, one binding lists at most 50 tools, and OpenAI and Azure OpenAI accept at most 128 tools in one request. */}} -{{- $agents := list - (dict - "name" "cluster-ops" - "description" "Kubernetes workload, Helm release, Argo Rollouts, and Prometheus metrics assistant." - "skills" (list "k8s-troubleshooting" "helm-release-management" "argo-rollouts-progressive-delivery" "prometheus-observability") - "tools" (list - "k8s_apply_manifest" "k8s_check_service_connectivity" "k8s_delete_resource" "k8s_describe_resource" - "k8s_get_events" "k8s_get_pod_logs" "k8s_get_resource_yaml" "k8s_get_resources" "k8s_patch_resource" - "k8s_rollout" "k8s_scale" - "helm_get_release" "helm_list_releases" "helm_repo_add" "helm_repo_update" "helm_uninstall" "helm_upgrade" - "argo_pause_rollout" "argo_promote_rollout" "argo_rollouts_list" "argo_set_rollout_image" - "argo_verify_argo_rollouts_controller_install" "argo_verify_gateway_plugin" "argo_verify_kubectl_plugin_install" - "prometheus_label_names_tool" "prometheus_promql_tool" "prometheus_query_range_tool" "prometheus_query_tool" - "prometheus_targets_tool")) - (dict - "name" "networking" - "description" "Istio service mesh and Cilium networking assistant." - "skills" (list "istio-traffic-debugging" "cilium-network-debugging") - "tools" (list - "k8s_apply_manifest" "k8s_describe_resource" "k8s_get_events" "k8s_get_pod_logs" "k8s_get_resource_yaml" - "k8s_get_resources" - "istio_analyze_cluster_configuration" "istio_apply_waypoint" "istio_delete_waypoint" "istio_list_waypoints" - "istio_proxy_config" "istio_proxy_status" "istio_version" "istio_waypoint_status" "istio_ztunnel_config" - "cilium_display_policy_node_information" "cilium_display_selectors" "cilium_flush_ipsec_state" - "cilium_get_daemon_status" "cilium_get_endpoint_details" "cilium_get_endpoints_list" - "cilium_get_identity_details" "cilium_get_service_information" "cilium_list_bgp_peers" - "cilium_list_bgp_routes" "cilium_list_cluster_nodes" "cilium_list_services" - "cilium_show_cluster_mesh_status" "cilium_status_and_version" "cilium_uninstall_cilium" - "cilium_validate_cilium_network_policies")) - (dict - "name" "security" - "description" "Kubescape security posture assistant." - "skills" (list "kubescape-security-posture") - "tools" (list - "kubescape_check_health" "kubescape_get_application_profile" "kubescape_get_configuration_scan" - "kubescape_get_network_neighborhood" "kubescape_get_vulnerability_details" - "kubescape_list_application_profiles" "kubescape_list_configuration_scans" - "kubescape_list_network_neighborhoods" "kubescape_list_vulnerabilities" - "kubescape_list_vulnerability_manifests")) -}} apiVersion: api.kagent.dev/v1alpha3 kind: Harness metadata: @@ -80,23 +40,22 @@ spec: name: {{ .Values.substrateWorkerPool.name | quote }} snapshotPolicy: location: {{ .Values.exampleAgents.snapshotLocation | quote }} -{{- range $agents }} --- apiVersion: api.kagent.dev/v1alpha3 kind: Agent metadata: - name: {{ printf "%s-%s" $prefix .name | quote }} - namespace: {{ include "kagent.namespace" $ }} + name: {{ printf "%s-cluster-ops" $prefix | quote }} + namespace: {{ include "kagent.namespace" . }} labels: - {{- include "kagent.labels" $ | nindent 4 }} + {{- include "kagent.labels" . | nindent 4 }} spec: harnessRef: name: {{ printf "%s-example" $prefix | quote }} template: inline: - description: {{ .description | quote }} + description: "Kubernetes workload, Helm release, Argo Rollouts, and Prometheus metrics assistant." modelConfig: - name: {{ include "kagent.defaultModelConfigName" $ }} + name: {{ include "kagent.defaultModelConfigName" . }} promptTemplate: dataSources: - name: kagent-builtin-prompts @@ -119,12 +78,172 @@ spec: kind: RemoteMCPServer name: {{ $toolServer }} tools: - {{- toYaml .tools | nindent 14 }} + - k8s_apply_manifest + - k8s_check_service_connectivity + - k8s_delete_resource + - k8s_describe_resource + - k8s_get_events + - k8s_get_pod_logs + - k8s_get_resource_yaml + - k8s_get_resources + - k8s_patch_resource + - k8s_rollout + - k8s_scale + - helm_get_release + - helm_list_releases + - helm_repo_add + - helm_repo_update + - helm_uninstall + - helm_upgrade + - argo_pause_rollout + - argo_promote_rollout + - argo_rollouts_list + - argo_set_rollout_image + - argo_verify_argo_rollouts_controller_install + - argo_verify_gateway_plugin + - argo_verify_kubectl_plugin_install + - prometheus_label_names_tool + - prometheus_promql_tool + - prometheus_query_range_tool + - prometheus_query_tool + - prometheus_targets_tool plugins: - source: oci: {{ $skillsImage | quote }} path: opt/kagent-tools-skills skills: - {{- toYaml .skills | nindent 12 }} -{{- end }} + - k8s-troubleshooting + - helm-release-management + - argo-rollouts-progressive-delivery + - prometheus-observability +--- +apiVersion: api.kagent.dev/v1alpha3 +kind: Agent +metadata: + name: {{ printf "%s-networking" $prefix | quote }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + harnessRef: + name: {{ printf "%s-example" $prefix | quote }} + template: + inline: + description: "Istio service mesh and Cilium networking assistant." + modelConfig: + name: {{ include "kagent.defaultModelConfigName" . }} + promptTemplate: + dataSources: + - name: kagent-builtin-prompts + alias: builtin + systemPrompt: | + Answer questions about the Kubernetes cluster where kagent runs. When a + question matches one of your skills, load that skill and follow it. Each + skill names the kagent-tools MCP tools it uses. + + {{`{{include "builtin/skills-usage"}}`}} + + {{`{{include "builtin/kubernetes-context"}}`}} + + {{`{{include "builtin/tool-usage-best-practices"}}`}} + + {{`{{include "builtin/safety-guardrails"}}`}} + tools: + - mcp: + server: + kind: RemoteMCPServer + name: {{ $toolServer }} + tools: + - k8s_apply_manifest + - k8s_describe_resource + - k8s_get_events + - k8s_get_pod_logs + - k8s_get_resource_yaml + - k8s_get_resources + - istio_analyze_cluster_configuration + - istio_apply_waypoint + - istio_delete_waypoint + - istio_list_waypoints + - istio_proxy_config + - istio_proxy_status + - istio_version + - istio_waypoint_status + - istio_ztunnel_config + - cilium_display_policy_node_information + - cilium_display_selectors + - cilium_flush_ipsec_state + - cilium_get_daemon_status + - cilium_get_endpoint_details + - cilium_get_endpoints_list + - cilium_get_identity_details + - cilium_get_service_information + - cilium_list_bgp_peers + - cilium_list_bgp_routes + - cilium_list_cluster_nodes + - cilium_list_services + - cilium_show_cluster_mesh_status + - cilium_status_and_version + - cilium_uninstall_cilium + - cilium_validate_cilium_network_policies + plugins: + - source: + oci: {{ $skillsImage | quote }} + path: opt/kagent-tools-skills + skills: + - istio-traffic-debugging + - cilium-network-debugging +--- +apiVersion: api.kagent.dev/v1alpha3 +kind: Agent +metadata: + name: {{ printf "%s-security" $prefix | quote }} + namespace: {{ include "kagent.namespace" . }} + labels: + {{- include "kagent.labels" . | nindent 4 }} +spec: + harnessRef: + name: {{ printf "%s-example" $prefix | quote }} + template: + inline: + description: "Kubescape security posture assistant." + modelConfig: + name: {{ include "kagent.defaultModelConfigName" . }} + promptTemplate: + dataSources: + - name: kagent-builtin-prompts + alias: builtin + systemPrompt: | + Answer questions about the Kubernetes cluster where kagent runs. When a + question matches one of your skills, load that skill and follow it. Each + skill names the kagent-tools MCP tools it uses. + + {{`{{include "builtin/skills-usage"}}`}} + + {{`{{include "builtin/kubernetes-context"}}`}} + + {{`{{include "builtin/tool-usage-best-practices"}}`}} + + {{`{{include "builtin/safety-guardrails"}}`}} + tools: + - mcp: + server: + kind: RemoteMCPServer + name: {{ $toolServer }} + tools: + - kubescape_check_health + - kubescape_get_application_profile + - kubescape_get_configuration_scan + - kubescape_get_network_neighborhood + - kubescape_get_vulnerability_details + - kubescape_list_application_profiles + - kubescape_list_configuration_scans + - kubescape_list_network_neighborhoods + - kubescape_list_vulnerabilities + - kubescape_list_vulnerability_manifests + plugins: + - source: + oci: {{ $skillsImage | quote }} + path: opt/kagent-tools-skills + skills: + - kubescape-security-posture {{- end }} From ac7c42e77e2ff2549fc61fa651a263b7518d7e8f Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Wed, 28 Oct 2026 23:23:24 -0400 Subject: [PATCH 11/12] feat(helm): default the example snapshot location and tighten the docs exampleAgents.snapshotLocation now defaults to gs://ate-snapshots/kagent/, the bucket that the Substrate chart's built-in rustfs store creates. A user who turned off rustfs sets their own bucket. Reword the example Agents comments, docs, and messages for plain technical writing: no semicolons, active voice, and one idea per sentence. Signed-off-by: Danielle Mustillo --- .../configuration-and-compilation.md | 15 +++--- docs/env.md | 2 +- go/core/pkg/env/testing.go | 2 +- go/core/test/e2e/README.md | 10 ++-- go/core/test/e2e/example_agent_skills_test.go | 13 ++--- helm/README.md | 54 ++++++++++--------- helm/kagent/templates/example-agents.yaml | 2 +- helm/kagent/tests/example-agents_test.yaml | 14 ++++- helm/kagent/values.yaml | 12 +++-- scripts/package-release-charts_test.sh | 5 +- scripts/tools-image-metadata.sh | 7 +-- 11 files changed, 79 insertions(+), 57 deletions(-) diff --git a/docs/architecture/configuration-and-compilation.md b/docs/architecture/configuration-and-compilation.md index 4ad7b9aa12..5c447f0a89 100644 --- a/docs/architecture/configuration-and-compilation.md +++ b/docs/architecture/configuration-and-compilation.md @@ -96,13 +96,14 @@ Local installations can override individual defaults with and explicit overrides. The chart's optional example Agents mount skills from the kagent-tools image. -That image is released from its own repository, so no build here records its -digest. Before packaging, `make helm-publish` runs `make tools-image-metadata`, -which resolves the digest of the kagent-tools version in the chart's -dependencies from the registry and writes it to `RUNTIME_IMAGE_METADATA_DIR`. -`make helm-release` validates that metadata when present and packages -`files/kagent-tools-image.json`. Without it, the chart renders the example -Agents only with an explicit `exampleAgents.skillsImage`. +kagent-tools releases from its own repository, so no build in this repository +records the image digest. Before packaging, `make helm-publish` runs `make +tools-image-metadata`. That target looks up the digest of the kagent-tools +version in the chart's dependencies and writes it to +`RUNTIME_IMAGE_METADATA_DIR/kagent-tools.json`. If that file exists, `make +helm-release` validates it and packages it as `files/kagent-tools-image.json`. +If the file is missing, the example Agents render only with an explicit +`exampleAgents.skillsImage`. The outer compiler sets the selected image on the compiled revision before hashing it. Adapters receive the requested Harness spec; provenance records that spec diff --git a/docs/env.md b/docs/env.md index 8bc030b020..91e5ad8b22 100644 --- a/docs/env.md +++ b/docs/env.md @@ -180,7 +180,7 @@ This reference covers user-configurable settings for the controller, CLI, standa | `KAGENT_E2E_RUN_UPGRADE_TESTS` | String | `false` | Run upgrade integration tests when exactly true. | | `KAGENT_E2E_SANDBOX_NAMESPACE` | String | `kagent` | Namespace for sandbox E2E resources. | | `KAGENT_E2E_SANDBOX_WORKER_POOL` | String | `kagent-default` | Worker pool used by sandbox E2E resources. | -| `KAGENT_E2E_TOOLS_SKILLS_IMAGE` | String | `(none)` | Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agents E2E test. That test is skipped when unset. | +| `KAGENT_E2E_TOOLS_SKILLS_IMAGE` | String | `(none)` | Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agents E2E test. When unset, that test skips. | | `KAGENT_E2E_UI_LOOP_EXTENSION_PORT` | String | `(none)` | Example-extension browser-test port. Defaults to KAGENT_E2E_UI_LOOP_PORT plus 50. | | `KAGENT_E2E_UI_LOOP_LIVE` | Boolean | `false` | Run UI browser tests against a real cluster when exactly true. | | `KAGENT_E2E_UI_LOOP_LIVE_PORT` | String | `8301` | Live-cluster UI browser-test port. | diff --git a/go/core/pkg/env/testing.go b/go/core/pkg/env/testing.go index 9a5bf21f58..11ff88e8c4 100644 --- a/go/core/pkg/env/testing.go +++ b/go/core/pkg/env/testing.go @@ -56,7 +56,7 @@ var ( _ = RegisterStringVar("KAGENT_E2E_BYO_IMAGE", "", "Digest-pinned BYO image substituted into E2E lifecycle manifests.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_CLAUDE_IMAGE", "", "Digest-pinned Claude harness image substituted into E2E lifecycle manifests.", ComponentTesting) _ = RegisterStringVar("KAGENT_E2E_CODEX_IMAGE", "", "Digest-pinned Codex harness image substituted into E2E lifecycle manifests.", ComponentTesting) - E2EToolsSkillsImage = RegisterStringVar("KAGENT_E2E_TOOLS_SKILLS_IMAGE", "", "Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agents E2E test. That test is skipped when unset.", ComponentTesting) + E2EToolsSkillsImage = RegisterStringVar("KAGENT_E2E_TOOLS_SKILLS_IMAGE", "", "Digest-pinned kagent-tools image with the opt/kagent-tools-skills Agent Plugin for the example Agents E2E test. When unset, that test skips.", ComponentTesting) E2ESandboxNamespace = RegisterStringVar("KAGENT_E2E_SANDBOX_NAMESPACE", "kagent", "Namespace for sandbox E2E resources.", ComponentTesting) E2ESandboxWorkerPool = RegisterStringVar("KAGENT_E2E_SANDBOX_WORKER_POOL", "kagent-default", "Worker pool used by sandbox E2E resources.", ComponentTesting) E2EOTLPListenAddress = RegisterStringVar("KAGENT_E2E_OTLP_LISTEN_ADDRESS", "", "Listen address for the E2E suite's OTLP trace receiver. Unset disables the shared receiver.", ComponentTesting) diff --git a/go/core/test/e2e/README.md b/go/core/test/e2e/README.md index fcd591c52f..0ce961a903 100644 --- a/go/core/test/e2e/README.md +++ b/go/core/test/e2e/README.md @@ -10,11 +10,11 @@ the controller leaves the user spec unchanged. `TestExampleAgentToolsSkills` runs each of the chart's three example Agents on every harness with a mock model. It reads each Agent's name, description, and skills from `helm/kagent/templates/example-agents.yaml`. It needs -`KAGENT_E2E_TOOLS_SKILLS_IMAGE`, a digest-pinned kagent-tools image that -contains the `opt/kagent-tools-skills` Agent Plugin, in the local test registry; -the test is skipped without it. For each Agent it checks that exactly the -Agent's own skills are advertised and that one of them loads. It omits the tool -server binding because the test install does not run kagent-tools. +`KAGENT_E2E_TOOLS_SKILLS_IMAGE`: a digest-pinned kagent-tools image in the local +test registry that contains the `opt/kagent-tools-skills` Agent Plugin. Without +that variable, the test skips. For each Agent, the test checks that the model +sees only the Agent's own skills and that one of them loads. The test leaves +out the tool server binding because the test install does not run kagent-tools. The standalone sandbox suite needs a Substrate WorkerPool with available capacity. Set `config.sandboxes.guestImage` to the diff --git a/go/core/test/e2e/example_agent_skills_test.go b/go/core/test/e2e/example_agent_skills_test.go index 7d299694e4..4ac5f8fa86 100644 --- a/go/core/test/e2e/example_agent_skills_test.go +++ b/go/core/test/e2e/example_agent_skills_test.go @@ -72,16 +72,17 @@ func readExampleAgents(t *testing.T) []exampleAgent { return agents } -// TestExampleAgentToolsSkills runs each of the chart's example Agent shapes on -// every harness with a mock model: the kagent-tools image as an Agent Plugin, -// the Agent's own skills, and the built-in prompt library. The chart's tool -// server binding is left out because the E2E install does not run kagent-tools. +// TestExampleAgentToolsSkills builds each of the chart's example Agents on +// every harness with a mock model. Each test Agent gets the kagent-tools image +// as an Agent Plugin, the chart Agent's skills, and the built-in prompts. The +// test leaves out the chart's tool server binding because the E2E install does +// not run kagent-tools. func TestExampleAgentToolsSkills(t *testing.T) { t.Parallel() target := interactionTarget(t) image := kagentenv.E2EToolsSkillsImage.Get() if image == "" { - t.Skip("KAGENT_E2E_TOOLS_SKILLS_IMAGE is not set; it needs a kagent-tools image with the opt/kagent-tools-skills folder") + t.Skip("KAGENT_E2E_TOOLS_SKILLS_IMAGE is not set. Set it to a kagent-tools image with the opt/kagent-tools-skills folder.") } agents := readExampleAgents(t) for _, agent := range agents { @@ -128,7 +129,7 @@ func TestExampleAgentToolsSkills(t *testing.T) { require.Equal(t, a2atype.TaskStateCompleted, task.Status.State, taskText(task)) require.Contains(t, taskText(task), "EXAMPLE_SKILL_LOADED") - // Claude also sends requests without a body; only model turns count. + // Claude also sends requests without a body. Count only model turns. requests := slices.DeleteFunc(model.Requests("", ""), func(r recordedModelRequest) bool { return len(r.Body) == 0 }) require.GreaterOrEqual(t, len(requests), 2, "the model must be called again after loading the skill") first := string(requests[0].Body) diff --git a/helm/README.md b/helm/README.md index 8cc1bad470..8d420c434b 100644 --- a/helm/README.md +++ b/helm/README.md @@ -233,14 +233,14 @@ workers. kagent does not install these prerequisites. #### Example Agents -Set `exampleAgents.enabled: true` and `exampleAgents.snapshotLocation` to -install three example Agents. They are off by default. The chart then creates: +Set `exampleAgents.enabled: true` to install three example Agents. They are off +by default. The chart then creates: -- a Harness named `-example` (default `kagent-tools-example`) that - runs the Go ADK runtime from the release's Harness image catalog, on the - `substrateWorkerPool.name` WorkerPool; -- three Agents on that Harness, each with skills from the kagent-tools image at - `opt/kagent-tools-skills`: +- A Harness named `-example` (default `kagent-tools-example`). It + runs the Go ADK image from the release's Harness image catalog on the + `substrateWorkerPool.name` WorkerPool. +- Three Agents on that Harness. Each Agent loads its skills from + `opt/kagent-tools-skills` in the kagent-tools image: | Agent | Skills | Tools | |---|---|---| @@ -251,41 +251,47 @@ install three example Agents. They are off by default. The chart then creates: Each Agent uses `default-model-config` and includes the `skills-usage`, `kubernetes-context`, `tool-usage-best-practices`, and `safety-guardrails` prompts from the `kagent-builtin-prompts` ConfigMap. Each skill names the -kagent-tools MCP tools it uses, and each Agent binds only those tools from the -chart's `-tool-server` RemoteMCPServer, in one binding. The Agents need +kagent-tools MCP tools it uses. Each Agent binds only those tools, in one +binding to the chart's `-tool-server` RemoteMCPServer. The Agents need `kagent-tools.enabled: true`. -The skills are split over three Agents to keep every Agent within three limits: -one binding lists at most 50 tools, the Claude and Codex harnesses accept each -MCP server only once, and OpenAI and Azure OpenAI accept at most 128 tools in -one request. The seven skills together name 64 tools, and the whole tool server -has more than 128. +The chart splits the skills over three Agents because one Agent cannot hold all +seven. The seven skills name 64 tools, and three limits apply: + +- One binding lists at most 50 tools. +- The Claude and Codex harnesses accept each MCP server only once. +- OpenAI and Azure OpenAI accept at most 128 tools in one request. The whole + tool server has more than 128. The skills come from the kagent-tools image, pinned by digest. `make -helm-publish` resolves the digest of the kagent-tools version in the chart's -dependencies (`make tools-image-metadata`) and packages it in -`files/kagent-tools-image.json`. A source chart has no packaged digest. With a -source chart, or to use another image, set `exampleAgents.skillsImage`: +helm-publish` runs `make tools-image-metadata`, which resolves the digest of the +kagent-tools version in the chart's dependencies. It then packages that digest +in `files/kagent-tools-image.json`. A source chart has no packaged digest. To +install from a source chart, or to use another image, set +`exampleAgents.skillsImage`: ```yaml exampleAgents: enabled: true - snapshotLocation: gs:///kagent/ skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256: ``` Without a digest, the chart fails to render. `global.imageRegistry` rewrites -the packaged digest and the override. +the registry of both the packaged image and `exampleAgents.skillsImage`. The skills folder ships in kagent-tools releases that include [kagent-dev/tools#91](https://github.com/kagent-dev/tools/pull/91). Until the chart depends on such a release, set `exampleAgents.skillsImage` to an image -built from it. +built from #91. + +The Harness saves snapshots to `exampleAgents.snapshotLocation`, by default +`gs://ate-snapshots/kagent/`. The Substrate chart's built-in rustfs store +creates that bucket. If you turned off rustfs, set your own bucket. -Notes: +Two limits apply: -- The example uses an OCI source only. Git sources currently fail to load in - the agent runtime, and S3 sources are untested. +- The example uses an OCI source only. Git sources fail to load in the agent + runtime today, and nobody has tested S3 sources. - Skill selection is not a security boundary. Substrate mounts the whole image read-only, so the agent can still read the files of skills it did not select. diff --git a/helm/kagent/templates/example-agents.yaml b/helm/kagent/templates/example-agents.yaml index 625f531f3e..7b726aaa45 100644 --- a/helm/kagent/templates/example-agents.yaml +++ b/helm/kagent/templates/example-agents.yaml @@ -13,7 +13,7 @@ {{- end }} {{- end }} {{- if not $skillsImage }} -{{- fail "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgents.skillsImage to @sha256:." }} +{{- fail "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills. Otherwise, set exampleAgents.skillsImage to @sha256:." }} {{- end }} {{- if not (regexMatch "^[^[:space:]@]+@sha256:[0-9a-f]{64}$" $skillsImage) }} {{- fail (printf "exampleAgents.skillsImage must be pinned by sha256 digest: %q" $skillsImage) }} diff --git a/helm/kagent/tests/example-agents_test.yaml b/helm/kagent/tests/example-agents_test.yaml index 967ab59b98..ca868bd91a 100644 --- a/helm/kagent/tests/example-agents_test.yaml +++ b/helm/kagent/tests/example-agents_test.yaml @@ -6,7 +6,6 @@ release: namespace: kagent set: exampleAgents.enabled: true - exampleAgents.snapshotLocation: gs://bucket/kagent/ exampleAgents.skillsImage: ghcr.io/kagent-dev/kagent/tools@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd tests: - it: should render nothing by default @@ -43,6 +42,17 @@ tests: - equal: path: spec.substrate.workerPoolRef.name value: kagent-default + - equal: + path: spec.substrate.snapshotPolicy.location + value: gs://ate-snapshots/kagent/ + + - it: should use a custom snapshot location + documentSelector: + path: kind + value: Harness + set: + exampleAgents.snapshotLocation: gs://bucket/kagent/ + asserts: - equal: path: spec.substrate.snapshotPolicy.location value: gs://bucket/kagent/ @@ -185,7 +195,7 @@ tests: exampleAgents.skillsImage: "" asserts: - failedTemplate: - errorMessage: "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills; otherwise set exampleAgents.skillsImage to @sha256:." + errorMessage: "exampleAgents.enabled requires the kagent-tools image digest. Release charts package it when their kagent-tools version ships the skills. Otherwise, set exampleAgents.skillsImage to @sha256:." - it: should fail when the skills image is not pinned by digest set: diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index c5d8f4e1e7..dc6ad4059e 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -776,7 +776,7 @@ kagent-tools: # ============================================================================== # Three Agents (cluster-ops, networking, security) on one Go ADK Harness. They # use the default ModelConfig, the kagent-tools MCP server, and the seven skills -# packaged in the kagent-tools image under opt/kagent-tools-skills. Requires +# packaged in the kagent-tools image under opt/kagent-tools-skills. They need # kagent-tools.enabled. exampleAgents: # -- Install the example Agents and their Harness. @@ -787,12 +787,14 @@ exampleAgents: # -- Digest-pinned kagent-tools image that holds the skills, for example # ghcr.io/kagent-dev/kagent/tools@sha256:. Empty uses the digest that # release packaging resolved for the kagent-tools version this chart pins. - # Source charts have no packaged digest, so set this value to use them. + # A source chart has no packaged digest, so set this value when you install + # from source. # global.imageRegistry rewrites both the packaged default and this override. skillsImage: "" - # -- Snapshot storage location for the example Harness, for example - # gs:///kagent/. Required when enabled. - snapshotLocation: "" + # -- Snapshot storage location for the example Harness. The default is the + # ate-snapshots bucket that the Substrate chart's built-in rustfs store + # creates. If you turned off rustfs, set your own bucket. + snapshotLocation: gs://ate-snapshots/kagent/ # ============================================================================== # PROXY CONFIGURATION diff --git a/scripts/package-release-charts_test.sh b/scripts/package-release-charts_test.sh index 9031ec0ec4..80f07a06fd 100644 --- a/scripts/package-release-charts_test.sh +++ b/scripts/package-release-charts_test.sh @@ -175,8 +175,9 @@ fi [[ "$(< "$scratch/command.log")" == *'requires the kagent-tools image digest'* ]] mv "$scratch/kagent-tools.json" "$metadata_dir/kagent-tools.json" -# kagent-tools is released from its own repository, so its digest is the one -# registry lookup in release packaging. A fixture stands in for the registry. +# Release packaging looks up one digest in a registry: the kagent-tools image, +# because kagent-tools releases from its own repository. A fake docker command +# stands in for the registry. mkdir -p "$scratch/tools-bin" "$scratch/tools-source/kagent" cat > "$scratch/tools-bin/docker" <<'EOF' #!/usr/bin/env bash diff --git a/scripts/tools-image-metadata.sh b/scripts/tools-image-metadata.sh index 089c484f46..a649d1d573 100644 --- a/scripts/tools-image-metadata.sh +++ b/scripts/tools-image-metadata.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -# Called by make tools-image-metadata. kagent-tools is released from its own -# repository, so no build here records its digest; resolve it from the registry. +# Called by make tools-image-metadata. kagent-tools releases from its own +# repository, so no build in this repository records the image digest. This +# script looks it up in the registry. chart_template="${1:?kagent Chart-template.yaml is required}" repository="${2:?kagent-tools image repository is required}" output="${3:?output file is required}" @@ -19,7 +20,7 @@ fi # no digest for them, so the example Agents need an explicit skillsImage. last_without_skills="0.3.0" if [[ "$(printf '%s\n%s\n' "$version" "$last_without_skills" | sort -V | tail -n1)" == "$last_without_skills" ]]; then - echo "kagent-tools $version has no skills folder; recording no digest" >&2 + echo "kagent-tools $version has no skills folder, so no digest is recorded" >&2 exit 0 fi # The kagent-tools chart tags its image with the chart version. From fa789f3fe7dc22a1c5346d10ed5f5394bf58638a Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Thu, 29 Oct 2026 00:01:00 -0400 Subject: [PATCH 12/12] docs(helm): state the kagent-tools version rule instead of a PR link A PR number goes stale once kagent-tools ships the skills. Say that kagent-tools 0.3.0 and earlier have no skills folder, which is the rule release packaging enforces. Signed-off-by: Danielle Mustillo --- helm/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/helm/README.md b/helm/README.md index 8d420c434b..56eb56d8ce 100644 --- a/helm/README.md +++ b/helm/README.md @@ -279,10 +279,9 @@ exampleAgents: Without a digest, the chart fails to render. `global.imageRegistry` rewrites the registry of both the packaged image and `exampleAgents.skillsImage`. -The skills folder ships in kagent-tools releases that include -[kagent-dev/tools#91](https://github.com/kagent-dev/tools/pull/91). Until the -chart depends on such a release, set `exampleAgents.skillsImage` to an image -built from #91. +kagent-tools 0.3.0 and earlier have no `opt/kagent-tools-skills` folder, so +release packaging records no digest for them. While the chart depends on such a +version, set `exampleAgents.skillsImage` to an image that has the folder. The Harness saves snapshots to `exampleAgents.snapshotLocation`, by default `gs://ate-snapshots/kagent/`. The Substrate chart's built-in rustfs store