From b417a4a287fde44640d764614a5c4dc248e9c318 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Thu, 29 Oct 2026 12:41:47 -0400 Subject: [PATCH 1/3] feat: publish the skills-only plugin root as its own image An Agent that mounts opt/kagent-tools-skills from the tools image pulls the whole tools image (195 MiB compressed) to read the skills. Build the same root as ghcr.io/kagent-dev/kagent/tools-skills, a one-layer image from scratch of about 5 KiB compressed, and push it on release. - Dockerfile: a skills stage above the final stage, so the tools image build is unchanged. - Makefile: docker-build-skills; plugin-verify builds and checks both. - verify-plugin-image.sh and TestSkillsImageMatchesToolsImage: the skills image holds exactly the tools image's skills root. - tag.yaml pushes the skills image; docs point skills-only mode at it. Closes #97 Signed-off-by: Danielle Mustillo --- .github/workflows/ci.yaml | 2 +- .github/workflows/tag.yaml | 1 + Dockerfile | 9 ++++++ Makefile | 10 ++++-- README.md | 9 +++--- cmd/plugin_docs_test.go | 7 +++- cmd/plugin_image_test.go | 59 ++++++++++++++++++++++++++++++++++ docs/kagent-skills.md | 36 ++++++++++++--------- scripts/verify-plugin-image.sh | 28 ++++++++++++---- 9 files changed, 131 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d98080bb..5c09230c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,7 +35,7 @@ jobs: --output=type=tar,dest=/dev/null DOCKER_REPO: "${{ github.repository_owner }}/tools" DOCKER_BUILDER: "docker buildx" - run: make docker-build + run: make docker-build docker-build-skills working-directory: ./ go-unit-tests: diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index a1fceee3..d16f04ab 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -51,6 +51,7 @@ jobs: fi make docker-build make helm-publish + make docker-build-skills release: # Only run release after images and helm chart are pushed # In the future we can take the chart from the helm action, diff --git a/Dockerfile b/Dockerfile index a9ce921a..2d545f9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,6 +107,15 @@ RUN set -eu; \ done; \ chown -R 65532:65532 /out/opt +# Skills-only image (tools-skills): the skills root alone, so an Agent that +# mounts it does not pull the CLIs. The last stage stays the default target. +FROM scratch AS skills +ARG VERSION +COPY --from=plugin /out/opt/kagent-tools-skills /opt/kagent-tools-skills +LABEL org.opencontainers.image.source=https://github.com/kagent-dev/tools +LABEL org.opencontainers.image.description="kagent Agent Plugin with the kagent-tools skills only" +LABEL org.opencontainers.image.version="$VERSION" + FROM gcr.io/distroless/static:nonroot WORKDIR / diff --git a/Makefile b/Makefile index b82be03a..e36feee8 100644 --- a/Makefile +++ b/Makefile @@ -121,6 +121,8 @@ retag: docker-build helm-version TOOLS_IMAGE_NAME ?= tools TOOLS_IMAGE_TAG ?= $(VERSION) TOOLS_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(TOOLS_IMAGE_NAME):$(TOOLS_IMAGE_TAG) +SKILLS_IMAGE_NAME ?= tools-skills +SKILLS_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(SKILLS_IMAGE_NAME):$(TOOLS_IMAGE_TAG) RETAGGED_DOCKER_REGISTRY = cr.kagent.dev RETAGGED_TOOLS_IMG = $(RETAGGED_DOCKER_REGISTRY)/$(DOCKER_REPO)/$(TOOLS_IMAGE_NAME):$(TOOLS_IMAGE_TAG) @@ -161,9 +163,13 @@ buildx-create: docker-build: fmt buildx-create $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(TOOLS_IMG) -f Dockerfile ./ +.PHONY: docker-build-skills # build the skills-only image +docker-build-skills: buildx-create + $(DOCKER_BUILDER) build $(DOCKER_BUILD_ARGS) --build-arg VERSION=$(VERSION) --target skills -t $(SKILLS_IMG) -f Dockerfile ./ + .PHONY: plugin-verify -plugin-verify: docker-build ## Verify the image is a valid kagent Agent Plugin (layout, limits, stdio) - ./scripts/verify-plugin-image.sh $(TOOLS_IMG) +plugin-verify: docker-build docker-build-skills ## Verify the images are valid kagent Agent Plugins (layout, limits, stdio) + ./scripts/verify-plugin-image.sh $(TOOLS_IMG) $(SKILLS_IMG) .PHONY: docker-build # build tools image for amd64 and arm64 docker-build-all: fmt buildx-create diff --git a/README.md b/README.md index d80732ca..d168f74f 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,11 @@ helm ls -A - **kagent 1.x skills bundle (OCI image)** -The same image is a kagent Agent Plugin: mount it in an `AgentTemplate` with `source.oci` -(digest-pinned) and `path: opt/kagent-tools-skills` to get the operational skills, and bind the -tools through a `RemoteMCPServer`. The full root, `opt/kagent-tools` (MCP tools, all CLIs and -skills in one bundle), is over kagent's 100 MiB package limit and needs a future kagent release. +The skills ship as a kagent Agent Plugin: mount `ghcr.io/kagent-dev/kagent/tools-skills` in an +`AgentTemplate` with `source.oci` (digest-pinned) and `path: opt/kagent-tools-skills` to get the +operational skills, and bind the tools through a `RemoteMCPServer`. The full root, +`opt/kagent-tools` in `ghcr.io/kagent-dev/kagent/tools` (MCP tools, all CLIs and skills in one +bundle), is over kagent's 100 MiB package limit and needs a future kagent release. See [Using kagent-tools as a kagent 1.x skills bundle](docs/kagent-skills.md). ## Quickstart Guide diff --git a/cmd/plugin_docs_test.go b/cmd/plugin_docs_test.go index 635821e9..36f25365 100644 --- a/cmd/plugin_docs_test.go +++ b/cmd/plugin_docs_test.go @@ -86,7 +86,12 @@ func TestSkillsGuideAgentTemplatesAreValid(t *testing.T) { } for _, plugin := range template.Spec.Plugins { - require.Contains(t, plugin.Source.OCI, "@sha256:", "example %d: pin the image by digest", index) + image := map[string]string{ + skillsOnlyRoot: "ghcr.io/kagent-dev/kagent/tools-skills@sha256:", + "opt/kagent-tools": "ghcr.io/kagent-dev/kagent/tools@sha256:", + }[plugin.Source.Path] + require.NotEmptyf(t, image, "example %d: unknown plugin root %q", index, plugin.Source.Path) + require.Truef(t, strings.HasPrefix(plugin.Source.OCI, image), "example %d: %s must come from %s", index, plugin.Source.Path, image) if plugin.Source.Path != skillsOnlyRoot || len(boundTools) == 0 { continue } diff --git a/cmd/plugin_image_test.go b/cmd/plugin_image_test.go index 699dba6d..f1f7d0a9 100644 --- a/cmd/plugin_image_test.go +++ b/cmd/plugin_image_test.go @@ -2,8 +2,10 @@ package main import ( "io/fs" + "maps" "os" "path/filepath" + "slices" "strings" "testing" @@ -95,3 +97,60 @@ func TestPluginImageLayout(t *testing.T) { require.Equal(t, expectedTarget, target, link) } } + +// TestSkillsImageMatchesToolsImage checks that the skills-only image ships +// the tools image's opt/kagent-tools-skills and nothing else. +func TestSkillsImageMatchesToolsImage(t *testing.T) { + toolsRoot := os.Getenv("KAGENT_TOOLS_IMAGE_ROOT") + skillsImageRoot := os.Getenv("KAGENT_TOOLS_SKILLS_IMAGE_ROOT") + if toolsRoot == "" || skillsImageRoot == "" { + t.Skip("set KAGENT_TOOLS_IMAGE_ROOT and KAGENT_TOOLS_SKILLS_IMAGE_ROOT to exported image filesystems (scripts/verify-plugin-image.sh)") + } + + // docker export adds .dockerenv, dev, etc, proc and sys to every container. + top, err := os.ReadDir(skillsImageRoot) + require.NoError(t, err) + for _, entry := range top { + require.Contains(t, []string{".dockerenv", "dev", "etc", "proc", "sys", "opt"}, entry.Name()) + } + opt, err := os.ReadDir(filepath.Join(skillsImageRoot, "opt")) + require.NoError(t, err) + require.Len(t, opt, 1) + require.Equal(t, "kagent-tools-skills", opt[0].Name()) + + want := snapshotTree(t, filepath.Join(toolsRoot, "opt", "kagent-tools-skills")) + got := snapshotTree(t, filepath.Join(skillsImageRoot, "opt", "kagent-tools-skills")) + require.NotEmpty(t, want) + require.Equal(t, slices.Sorted(maps.Keys(want)), slices.Sorted(maps.Keys(got))) + for path, entry := range want { + require.Equal(t, entry, got[path], path) + } +} + +// snapshotTree maps each path under root to its mode and, for files and +// symlinks, content or target. +func snapshotTree(t *testing.T, root string) map[string]string { + t.Helper() + tree := map[string]string{} + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + require.NoError(t, err) + rel, err := filepath.Rel(root, path) + require.NoError(t, err) + info, err := entry.Info() + require.NoError(t, err) + tree[rel] = info.Mode().String() + switch { + case info.Mode().IsRegular(): + content, err := os.ReadFile(path) + require.NoError(t, err) + tree[rel] += "\n" + string(content) + case info.Mode()&os.ModeSymlink != 0: + target, err := os.Readlink(path) + require.NoError(t, err) + tree[rel] += " -> " + target + } + return nil + }) + require.NoError(t, err) + return tree +} diff --git a/docs/kagent-skills.md b/docs/kagent-skills.md index a71c577e..5b0f86be 100644 --- a/docs/kagent-skills.md +++ b/docs/kagent-skills.md @@ -1,20 +1,22 @@ # Using kagent-tools as a kagent 1.x skills bundle -The kagent-tools image is also a kagent 1.x **Agent Plugin**: an agent can mount it read-only -as an OCI image volume to get a set of operational skills. Today, mount the skills-only root and -bind the tools through a `RemoteMCPServer`. The full root, which also ships the MCP server and all -CLIs in one bundle, needs a future kagent release (see [Requirements](#requirements)). +kagent-tools ships its skills as a kagent 1.x **Agent Plugin**: an agent mounts an image +read-only as an OCI image volume to get a set of operational skills. Today, mount the +skills-only image, `ghcr.io/kagent-dev/kagent/tools-skills`, and bind the tools through a +`RemoteMCPServer`. The full root, which also ships the MCP server and all CLIs in one bundle, +needs a future kagent release (see [Requirements](#requirements)). ## What you get -The image contains two plugin roots. Select **one** of them per agent. +Two images carry plugin roots. Select **one** root per agent. -| Root (`source.path`) | Contents | -|---|---| -| `opt/kagent-tools-skills` | `plugin.json` and `skills/` only (use with a `RemoteMCPServer`). Works on current kagent 1.x. | -| `opt/kagent-tools` | `plugin.json`, `mcp.json` (stdio MCP server), `bin/` (`kagent-tools`, `kubectl`, `helm`, `istioctl`, `kubectl-argo-rollouts`, `cilium`), `config/kubeconfig`, `skills/`. Needs a future kagent release. | +| Image | Root (`source.path`) | Contents | +|---|---|---| +| `ghcr.io/kagent-dev/kagent/tools-skills` (v0.4.0 and later) | `opt/kagent-tools-skills` | `plugin.json` and `skills/` only (use with a `RemoteMCPServer`). A few KiB. Works on current kagent 1.x. | +| `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools-skills` | The same skills-only root, inside the full tools image. Prefer the `tools-skills` image: mounting this root still pulls about 195 MiB. | +| `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools` | `plugin.json`, `mcp.json` (stdio MCP server), `bin/` (`kagent-tools`, `kubectl`, `helm`, `istioctl`, `kubectl-argo-rollouts`, `cilium`), `config/kubeconfig`, `skills/`. Needs a future kagent release. | -Skills (identical in both roots): +Skills (identical in every root): | Skill | Use it for | |---|---| @@ -39,16 +41,17 @@ Skills are playbooks that call the MCP tools by name (`k8s_get_resources`, `helm - For the full root (`opt/kagent-tools`): a kagent release that does not apply the 100 MiB package-size limit to platform-mounted OCI packages. The full root is about 550 MiB because it contains the CLIs. Older kagent releases must use [skills-only mode](#skills-only-mode). -- The image pinned by digest. Print it with: +- Each image pinned by digest. Print the digest with: ```bash + docker buildx imagetools inspect ghcr.io/kagent-dev/kagent/tools-skills: docker buildx imagetools inspect ghcr.io/kagent-dev/kagent/tools: ``` ## Skills-only mode This works on current kagent 1.x releases and with every harness. Deploy the server with the Helm -chart as usual, reference it as a `RemoteMCPServer`, and mount the skills-only root: +chart as usual, reference it as a `RemoteMCPServer`, and mount the skills-only image: ```yaml apiVersion: api.kagent.dev/v1alpha3 @@ -90,7 +93,7 @@ spec: - k8s_scale plugins: - source: - oci: ghcr.io/kagent-dev/kagent/tools@sha256: + oci: ghcr.io/kagent-dev/kagent/tools-skills@sha256: path: opt/kagent-tools-skills skills: - k8s-troubleshooting @@ -246,6 +249,7 @@ by `mcp.json` to the bundled kubeconfig) > in-cluster config. The resolved path make plugin-verify ``` -This builds the image, exports its filesystem, checks both roots against kagent's package rules -(file types, symlinks, entry count, skills-only size), and runs an MCP `initialize` + -`tools/list` over stdio to confirm stdout carries only JSON-RPC. +This builds the tools and tools-skills images, exports their filesystems, checks both roots +against kagent's package rules (file types, symlinks, entry count, skills-only size), checks that +the tools-skills image holds the same skills root as the tools image, and runs an MCP +`initialize` + `tools/list` over stdio to confirm stdout carries only JSON-RPC. diff --git a/scripts/verify-plugin-image.sh b/scripts/verify-plugin-image.sh index b75a3efc..6c791e56 100755 --- a/scripts/verify-plugin-image.sh +++ b/scripts/verify-plugin-image.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -# Verifies that a built kagent-tools image is a valid kagent Agent Plugin. +# Verifies that a built kagent-tools image is a valid kagent Agent Plugin and, +# when given, that the skills-only image holds the same skills root. set -euo pipefail -image="${1:?usage: $0 }" +image="${1:?usage: $0 []}" +skills_image="${2:-}" workdir="$(mktemp -d)" container="" cleanup() { @@ -11,10 +13,24 @@ cleanup() { } trap cleanup EXIT -container="$(docker create "$image")" -docker export "$container" | tar -x -C "$workdir" +export_image() { + # The skills image has no entrypoint; docker create needs a command but never runs it. + container="$(docker create "$1" /unused)" + mkdir -p "$2" + docker export "$container" | tar -x -C "$2" + docker rm "$container" >/dev/null + container="" +} -KAGENT_TOOLS_IMAGE_ROOT="$workdir" go test ./cmd -run TestPluginImageLayout -count=1 -v +export_image "$image" "$workdir/tools" +export KAGENT_TOOLS_IMAGE_ROOT="$workdir/tools" +tests=TestPluginImageLayout +if [[ -n "$skills_image" ]]; then + export_image "$skills_image" "$workdir/skills" + export KAGENT_TOOLS_SKILLS_IMAGE_ROOT="$workdir/skills" + tests="$tests|TestSkillsImageMatchesToolsImage" +fi +go test ./cmd -run "^($tests)\$" -count=1 -v echo "--- legacy entrypoints" docker run --rm --entrypoint /tool-server "$image" --version @@ -35,4 +51,4 @@ done if grep -qv '^{"jsonrpc"' <<<"$response"; then echo "non JSON-RPC output on stdout:"; grep -v '^{"jsonrpc"' <<<"$response" | head -5; exit 1 fi -echo "plugin image OK: $image" +echo "plugin image OK: $image${skills_image:+, skills image OK: $skills_image}" From cf0ee7529dae1f7b354bd002113a04c381964bbe Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Thu, 29 Oct 2026 13:00:35 -0400 Subject: [PATCH 2/3] docs: tighten skills image docs and comments Signed-off-by: Danielle Mustillo --- Dockerfile | 5 +++-- README.md | 10 +++++----- cmd/plugin_image_test.go | 8 ++++---- docs/kagent-skills.md | 20 ++++++++++---------- scripts/verify-plugin-image.sh | 6 +++--- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2d545f9f..4777fc96 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,8 +107,9 @@ RUN set -eu; \ done; \ chown -R 65532:65532 /out/opt -# Skills-only image (tools-skills): the skills root alone, so an Agent that -# mounts it does not pull the CLIs. The last stage stays the default target. +# The tools-skills image holds only the skills root, so an Agent that mounts it +# does not pull the CLIs. The last stage is the default build target, so keep +# this stage above it. FROM scratch AS skills ARG VERSION COPY --from=plugin /out/opt/kagent-tools-skills /opt/kagent-tools-skills diff --git a/README.md b/README.md index d168f74f..1ad9d66a 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,11 @@ helm ls -A - **kagent 1.x skills bundle (OCI image)** -The skills ship as a kagent Agent Plugin: mount `ghcr.io/kagent-dev/kagent/tools-skills` in an -`AgentTemplate` with `source.oci` (digest-pinned) and `path: opt/kagent-tools-skills` to get the -operational skills, and bind the tools through a `RemoteMCPServer`. The full root, -`opt/kagent-tools` in `ghcr.io/kagent-dev/kagent/tools` (MCP tools, all CLIs and skills in one -bundle), is over kagent's 100 MiB package limit and needs a future kagent release. +The operational skills ship as a kagent Agent Plugin image, `ghcr.io/kagent-dev/kagent/tools-skills`. +To use them, mount the image in an `AgentTemplate` with a digest-pinned `source.oci` and +`path: opt/kagent-tools-skills`, and bind the tools through a `RemoteMCPServer`. The full root, +`opt/kagent-tools` in `ghcr.io/kagent-dev/kagent/tools`, bundles the MCP tools, all CLIs, and the +skills. It is over kagent's 100 MiB package limit and needs a future kagent release. See [Using kagent-tools as a kagent 1.x skills bundle](docs/kagent-skills.md). ## Quickstart Guide diff --git a/cmd/plugin_image_test.go b/cmd/plugin_image_test.go index f1f7d0a9..29444a1d 100644 --- a/cmd/plugin_image_test.go +++ b/cmd/plugin_image_test.go @@ -107,11 +107,11 @@ func TestSkillsImageMatchesToolsImage(t *testing.T) { t.Skip("set KAGENT_TOOLS_IMAGE_ROOT and KAGENT_TOOLS_SKILLS_IMAGE_ROOT to exported image filesystems (scripts/verify-plugin-image.sh)") } - // docker export adds .dockerenv, dev, etc, proc and sys to every container. + // docker export adds .dockerenv, dev, etc, proc, and sys to every container. top, err := os.ReadDir(skillsImageRoot) require.NoError(t, err) for _, entry := range top { - require.Contains(t, []string{".dockerenv", "dev", "etc", "proc", "sys", "opt"}, entry.Name()) + require.Contains(t, []string{".dockerenv", "dev", "etc", "proc", "sys", "opt"}, entry.Name(), "unexpected top-level entry in the skills image") } opt, err := os.ReadDir(filepath.Join(skillsImageRoot, "opt")) require.NoError(t, err) @@ -127,8 +127,8 @@ func TestSkillsImageMatchesToolsImage(t *testing.T) { } } -// snapshotTree maps each path under root to its mode and, for files and -// symlinks, content or target. +// snapshotTree maps each path under root to its mode, plus the content of a +// file or the target of a symlink. func snapshotTree(t *testing.T, root string) map[string]string { t.Helper() tree := map[string]string{} diff --git a/docs/kagent-skills.md b/docs/kagent-skills.md index 5b0f86be..5c0059bf 100644 --- a/docs/kagent-skills.md +++ b/docs/kagent-skills.md @@ -1,10 +1,10 @@ # Using kagent-tools as a kagent 1.x skills bundle -kagent-tools ships its skills as a kagent 1.x **Agent Plugin**: an agent mounts an image -read-only as an OCI image volume to get a set of operational skills. Today, mount the -skills-only image, `ghcr.io/kagent-dev/kagent/tools-skills`, and bind the tools through a -`RemoteMCPServer`. The full root, which also ships the MCP server and all CLIs in one bundle, -needs a future kagent release (see [Requirements](#requirements)). +kagent-tools ships its operational skills as a kagent 1.x **Agent Plugin**. An agent mounts the +plugin image read-only, as an OCI image volume. Today, mount the skills-only image, +`ghcr.io/kagent-dev/kagent/tools-skills`, and bind the tools through a `RemoteMCPServer`. The +full root also ships the MCP server and all CLIs in one bundle. It needs a future kagent release +(see [Requirements](#requirements)). ## What you get @@ -13,7 +13,7 @@ Two images carry plugin roots. Select **one** root per agent. | Image | Root (`source.path`) | Contents | |---|---|---| | `ghcr.io/kagent-dev/kagent/tools-skills` (v0.4.0 and later) | `opt/kagent-tools-skills` | `plugin.json` and `skills/` only (use with a `RemoteMCPServer`). A few KiB. Works on current kagent 1.x. | -| `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools-skills` | The same skills-only root, inside the full tools image. Prefer the `tools-skills` image: mounting this root still pulls about 195 MiB. | +| `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools-skills` | The same skills-only root, inside the full tools image. Mounting this root pulls the whole 195 MiB image, so prefer `tools-skills`. | | `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools` | `plugin.json`, `mcp.json` (stdio MCP server), `bin/` (`kagent-tools`, `kubectl`, `helm`, `istioctl`, `kubectl-argo-rollouts`, `cilium`), `config/kubeconfig`, `skills/`. Needs a future kagent release. | Skills (identical in every root): @@ -249,7 +249,7 @@ by `mcp.json` to the bundled kubeconfig) > in-cluster config. The resolved path make plugin-verify ``` -This builds the tools and tools-skills images, exports their filesystems, checks both roots -against kagent's package rules (file types, symlinks, entry count, skills-only size), checks that -the tools-skills image holds the same skills root as the tools image, and runs an MCP -`initialize` + `tools/list` over stdio to confirm stdout carries only JSON-RPC. +This builds the tools and tools-skills images and exports their filesystems. It checks both roots +against kagent's package rules (file types, symlinks, entry count, skills-only size). It checks +that the tools-skills image holds the same skills root as the tools image. Last, it runs an MCP +`initialize` and `tools/list` over stdio to confirm that stdout carries only JSON-RPC. diff --git a/scripts/verify-plugin-image.sh b/scripts/verify-plugin-image.sh index 6c791e56..0410019e 100755 --- a/scripts/verify-plugin-image.sh +++ b/scripts/verify-plugin-image.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Verifies that a built kagent-tools image is a valid kagent Agent Plugin and, -# when given, that the skills-only image holds the same skills root. +# Verifies that a built kagent-tools image is a valid kagent Agent Plugin. Given a +# second image, it also checks that the skills-only image holds the same skills root. set -euo pipefail image="${1:?usage: $0 []}" @@ -14,7 +14,7 @@ cleanup() { trap cleanup EXIT export_image() { - # The skills image has no entrypoint; docker create needs a command but never runs it. + # The skills image has no command. docker create requires one but never runs it. container="$(docker create "$1" /unused)" mkdir -p "$2" docker export "$container" | tar -x -C "$2" From ef1554275f60cae7506e2e80f0e3c21f7fe04b24 Mon Sep 17 00:00:00 2001 From: Danielle Mustillo Date: Thu, 29 Oct 2026 13:10:34 -0400 Subject: [PATCH 3/3] docs: note the v0.4.0 cutoff and the tools-skills image in AGENTS.md Signed-off-by: Danielle Mustillo --- AGENTS.md | 6 ++++-- docs/kagent-skills.md | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a03e3f89..ef5d9e88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -362,13 +362,15 @@ Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `ci` ### Plugin bundle (kagent 1.x skills) -The image doubles as a kagent Agent Plugin (see [docs/kagent-skills.md](docs/kagent-skills.md)). +The tools image doubles as a kagent Agent Plugin, and `make docker-build-skills` builds the +skills-only root as its own image, `tools-skills` (see [docs/kagent-skills.md](docs/kagent-skills.md)). New tool providers should add or extend a skill in `plugin/skills//SKILL.md` and keep `cmd/plugin_bundle_test.go` passing; it checks manifests, `mcp.json`, skill frontmatter, that every tool a skill names is registered, and that every `` `argument: value` `` on a skill line is an input of a tool named on that line. `cmd/plugin_docs_test.go` checks the `AgentTemplate` examples in `docs/kagent-skills.md` against kagent's rules (`modelConfig`, at most 50 bound tools, and a -`tools:` list that covers the selected skills). Skills must only name tools that work with the +`tools:` list that covers the selected skills) and that each example mounts its root from the +right image. Skills must only name tools that work with the chart defaults (no tool that needs an unconfigured LLM key). Run `make plugin-verify` after changing the `Dockerfile` or `plugin/`. diff --git a/docs/kagent-skills.md b/docs/kagent-skills.md index 5c0059bf..559afea7 100644 --- a/docs/kagent-skills.md +++ b/docs/kagent-skills.md @@ -8,12 +8,12 @@ full root also ships the MCP server and all CLIs in one bundle. It needs a futur ## What you get -Two images carry plugin roots. Select **one** root per agent. +Two images carry plugin roots, starting with v0.4.0. Select **one** root per agent. | Image | Root (`source.path`) | Contents | |---|---|---| -| `ghcr.io/kagent-dev/kagent/tools-skills` (v0.4.0 and later) | `opt/kagent-tools-skills` | `plugin.json` and `skills/` only (use with a `RemoteMCPServer`). A few KiB. Works on current kagent 1.x. | -| `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools-skills` | The same skills-only root, inside the full tools image. Mounting this root pulls the whole 195 MiB image, so prefer `tools-skills`. | +| `ghcr.io/kagent-dev/kagent/tools-skills` | `opt/kagent-tools-skills` | `plugin.json` and `skills/` only (use with a `RemoteMCPServer`). A few KiB. Works on current kagent 1.x. | +| `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools-skills` | The same skills-only root, inside the full tools image. Mounting this root pulls the whole tools image, so prefer `tools-skills`. | | `ghcr.io/kagent-dev/kagent/tools` | `opt/kagent-tools` | `plugin.json`, `mcp.json` (stdio MCP server), `bin/` (`kagent-tools`, `kubectl`, `helm`, `istioctl`, `kubectl-argo-rollouts`, `cilium`), `config/kubeconfig`, `skills/`. Needs a future kagent release. | Skills (identical in every root):