diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 41771c07..85da96a0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -32,7 +32,7 @@ { "name": "ksail", "description": "KSail CLI expertise and MCP server — create and operate Kubernetes clusters (Kind, K3d, Talos, vCluster, KWOK, EKS) and GitOps workloads declaratively", - "version": "1.0.0", + "version": "1.0.1", "source": "./plugins/ksail" } ] diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 41771c07..85da96a0 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -32,7 +32,7 @@ { "name": "ksail", "description": "KSail CLI expertise and MCP server — create and operate Kubernetes clusters (Kind, K3d, Talos, vCluster, KWOK, EKS) and GitOps workloads declaratively", - "version": "1.0.0", + "version": "1.0.1", "source": "./plugins/ksail" } ] diff --git a/docs/installation.md b/docs/installation.md index 504667f4..1ea6bb90 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -54,7 +54,8 @@ Codex reads a plugin's `.codex-plugin/plugin.json` before its `.claude-plugin/pl `agentic-engineering` plugin ships one so that it can also offer the official Codex apps its agents use: iCloud Mail, iCloud Calendar, LinkedIn and Codex Security Cloud, all optional, listed in its [`.app.json`](../plugins/agentic-engineering/.app.json). Each still needs your own sign-in. -`scripts/validate-manifests.sh` keeps that manifest identical to the portable one apart from its +The `ksail` plugin ships one for its OpenAI plugin directory listing; it offers no apps. +`scripts/validate-manifests.sh` keeps each Codex manifest identical to the portable one apart from its component paths, app list and listing, and `scripts/bump-plugin-version.sh` moves its version with the others. diff --git a/plugins/ksail/.claude-plugin/icon.png b/plugins/ksail/.claude-plugin/icon.png new file mode 100644 index 00000000..b3340882 Binary files /dev/null and b/plugins/ksail/.claude-plugin/icon.png differ diff --git a/plugins/ksail/.claude-plugin/plugin.json b/plugins/ksail/.claude-plugin/plugin.json index 1850145a..9abf838d 100644 --- a/plugins/ksail/.claude-plugin/plugin.json +++ b/plugins/ksail/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ksail", "description": "KSail CLI expertise and MCP server — create and operate Kubernetes clusters (Kind, K3d, Talos, vCluster, KWOK, EKS) and GitOps workloads declaratively", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/ksail/.codex-plugin/plugin.json b/plugins/ksail/.codex-plugin/plugin.json new file mode 100644 index 00000000..caff0cdc --- /dev/null +++ b/plugins/ksail/.codex-plugin/plugin.json @@ -0,0 +1,51 @@ +{ + "name": "ksail", + "description": "KSail CLI expertise and MCP server — create and operate Kubernetes clusters (Kind, K3d, Talos, vCluster, KWOK, EKS) and GitOps workloads declaratively", + "version": "1.0.1", + "author": { + "name": "devantler-tech", + "url": "https://github.com/devantler-tech" + }, + "license": "PolyForm-Shield-1.0.0", + "homepage": "https://ksail.devantler.tech", + "repository": "https://github.com/devantler-tech/ksail", + "keywords": [ + "kubernetes", + "k8s", + "gitops", + "ksail", + "kind", + "k3d", + "talos", + "vcluster", + "eks", + "flux", + "argocd", + "mcp" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "KSail", + "shortDescription": "Create Kubernetes clusters", + "longDescription": "KSail is one CLI for creating and operating Kubernetes clusters and the GitOps workloads on them. This plugin teaches Codex to use it: scaffold a KSail project, create Kind, K3d, Talos, vCluster or KWOK clusters locally in Docker (Talos also on Hetzner and Omni), bootstrap Flux or ArgoCD, apply, validate and watch workloads, manage SOPS-encrypted secrets, and back up or restore cluster resources. It needs the ksail CLI on your PATH and, for local clusters, a running Docker daemon.", + "developerName": "Devantler Tech", + "category": "Developer Tools", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://ksail.devantler.tech/", + "supportURL": "https://github.com/devantler-tech/ksail/issues", + "defaultPrompt": [ + "Create a local Kubernetes cluster with KSail and Flux, then deploy the manifests in k8s/", + "Scaffold a KSail project for a Talos cluster and explain each file it writes", + "Encrypt the Kubernetes secrets in this repository with SOPS using ksail workload cipher" + ], + "brandColor": "#143150", + "brandColorDark": "#2EC4E6", + "composerIcon": "./.claude-plugin/icon.png", + "logo": "./.claude-plugin/icon.png" + } +} diff --git a/plugins/ksail/CHANGELOG.md b/plugins/ksail/CHANGELOG.md new file mode 100644 index 00000000..10795e63 --- /dev/null +++ b/plugins/ksail/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog — `ksail` + +## 1.0.1 — 2026-11-04 + +- Add the KSail app icon at `.claude-plugin/icon.png` (1024 × 1024 PNG, copied from + `desktop/resources/icon.png` in `devantler-tech/ksail`), so the Claude plugin directory can show + it as the listing icon. +- Add a Codex manifest at `.codex-plugin/plugin.json`. It repeats the portable manifest, loads the + skill and the `ksail` MCP server, and gives the OpenAI plugin directory its listing: name, short + and long descriptions, the Developer Tools category, website and support links, three example + prompts, brand colours and the same icon. diff --git a/plugins/ksail/plugin.json b/plugins/ksail/plugin.json index 1850145a..9abf838d 100644 --- a/plugins/ksail/plugin.json +++ b/plugins/ksail/plugin.json @@ -1,7 +1,7 @@ { "name": "ksail", "description": "KSail CLI expertise and MCP server — create and operate Kubernetes clusters (Kind, K3d, Talos, vCluster, KWOK, EKS) and GitOps workloads declaratively", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/scripts/guard-gh-json-fields.sh b/scripts/guard-gh-json-fields.sh index a41b2fc1..cc861f3c 100755 --- a/scripts/guard-gh-json-fields.sh +++ b/scripts/guard-gh-json-fields.sh @@ -14,7 +14,9 @@ # Usage: guard-gh-json-fields.sh [ROOT] (ROOT defaults to this repository) # Scans every *.md, *.txt, *.json, *.jq, *.go, *.yaml and *.yml under ROOT/plugins; any other non-script file is UNKNOWN. Shell # scripts are not scanned: a script with a bad field fails loudly the first time it runs, whereas prose -# silently misleads every agent that reads it. +# silently misleads every agent that reads it. Raster images (*.png, *.jpg, *.jpeg, *.webp, such as +# listing icons, anywhere under plugins/) are skipped only when they are regular files that begin with +# that format's signature; the bytes after the signature are not inspected. # Go 1.22+ is required: only the installed observer is built. Literal adjacent shell quotes are # joined without evaluating inspected text; expansions are UNKNOWN. JSON requires unique decoded # keys. Normalized text is bounded to 8 MiB. The Go decoder reads comments, decoded literal @@ -58,6 +60,24 @@ ensure_decoder() { fi } +# An agent opens a raster image as pixels, not as prose to follow, so it is not a surface this guard +# scans. Its name alone proves nothing: a plain text file called icon.png would hide its prescriptions. +# So an image is skipped only when it is a regular, unlinked file whose leading bytes are the +# signature of the format its extension names; anything else, including an unreadable file, is +# UNKNOWN. Only the signature is checked: text placed after it, or in a PNG text chunk, is not +# inspected, because no agent reads an image file as instructions. +raster_image() { + local path="$1" magic + [ -f "${path}" ] && [ ! -L "${path}" ] || return 1 + magic="$(LC_ALL=C od -An -N12 -tx1 "${path}" | tr -d ' \n')" || return 1 + case "${path}" in + *.png) [ "${magic:0:16}" = 89504e470d0a1a0a ] ;; + *.jpg | *.jpeg) [ "${magic:0:6}" = ffd8ff ] ;; + *.webp) [ "${magic:0:8}" = 52494646 ] && [ "${magic:16:8}" = 57454250 ] ;; + *) return 1 ;; + esac +} + # Emit the retained surface as plain text. $1 selects its type; $2 contains its observed bytes. # JSON is DECODED, never pattern-matched: a \u escape of a letter IS # that letter, so an escaped `merged` is still `merged`. Each decoded string is followed by `%`, which ends a field @@ -198,6 +218,9 @@ while IFS= read -r -d '' f; do case "$f" in *.md|*.txt|*.json|*.jq|*.go|*.yaml|*.yml) surfaces+=("$f") ;; *.sh) ;; + *.png | *.jpg | *.jpeg | *.webp) + raster_image "$f" || + unknown "${f#"${root}/"} is named as an image but is not a regular file with that image format's signature, so it may be text this guard does not scan" ;; *) unknown "${f#"${root}/"} is a file type this guard does not scan, so any field it prescribes would go unseen" ;; esac done < "${discovered}" diff --git a/scripts/guard-gh-json-fields.test.sh b/scripts/guard-gh-json-fields.test.sh index 2d2ff644..9e29dcb4 100755 --- a/scripts/guard-gh-json-fields.test.sh +++ b/scripts/guard-gh-json-fields.test.sh @@ -367,6 +367,48 @@ dir="$(fixture unknown-unscanned-type)" printf '%s\n' 'cmd: gh pr view 42 --json state,merged' > "${dir}/plugins/p/agents/case.ini" expect 2 "an unscanned file type is UNKNOWN" "${dir}" +# Listing icons are raster images: skipped only when their bytes carry that format's signature. +dir="$(fixture good-png-icon)" +mkdir -p "${dir}/plugins/p/.claude-plugin" +printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR' > "${dir}/plugins/p/.claude-plugin/icon.png" +expect 0 "a PNG with the PNG signature is skipped" "${dir}" +dir="$(fixture good-jpeg-icon)" +printf '\xff\xd8\xff\xe0\0\x10JFIF' > "${dir}/plugins/p/agents/logo.jpg" +printf '\xff\xd8\xff\xe0\0\x10JFIF' > "${dir}/plugins/p/agents/logo.jpeg" +expect 0 "a JPEG with the JPEG signature is skipped" "${dir}" +dir="$(fixture good-webp-icon)" +printf 'RIFF\x24\0\0\0WEBPVP8 ' > "${dir}/plugins/p/agents/logo.webp" +expect 0 "a WebP with the RIFF/WEBP signature is skipped" "${dir}" +# A name proves nothing: text called icon.png would carry prescriptions past the scan. +dir="$(fixture unknown-text-named-png)" +printf '%s\n' 'gh pr view 42 --json state,merged' > "${dir}/plugins/p/agents/icon.png" +expect 2 "text named .png is UNKNOWN, never skipped" "${dir}" +dir="$(fixture unknown-wrong-signature)" +printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR' > "${dir}/plugins/p/agents/icon.webp" +expect 2 "an image whose signature does not match its extension is UNKNOWN" "${dir}" +dir="$(fixture unknown-empty-png)" +: > "${dir}/plugins/p/agents/icon.png" +expect 2 "an empty .png is UNKNOWN" "${dir}" +dir="$(fixture unknown-symlinked-png)" +printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR' > "${dir}/real.png" +ln -s "${dir}/real.png" "${dir}/plugins/p/agents/icon.png" +expect 2 "a symlinked .png is UNKNOWN even when its target is an image" "${dir}" +dir="$(fixture unknown-fifo-png)" +mkfifo "${dir}/plugins/p/agents/icon.png" +expect 2 "a .png that is not a regular file (a FIFO) is UNKNOWN" "${dir}" +dir="$(fixture unknown-uppercase-png)" +printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR' > "${dir}/plugins/p/agents/icon.PNG" +expect 2 "an uppercase .PNG is not a recognised image extension and is UNKNOWN" "${dir}" +# An unreadable image cannot have its signature read, so it is UNKNOWN. Root reads it regardless of +# its mode, so the case only proves something when the file really is unreadable. +dir="$(fixture unknown-unreadable-png)" +printf '\x89PNG\r\n\x1a\n\0\0\0\rIHDR' > "${dir}/plugins/p/agents/icon.png" +chmod 000 "${dir}/plugins/p/agents/icon.png" +if [ ! -r "${dir}/plugins/p/agents/icon.png" ]; then + expect 2 "an unreadable .png is UNKNOWN" "${dir}" +fi +chmod 600 "${dir}/plugins/p/agents/icon.png" + # Native skill runtime metadata is YAML. Decode supported mappings rather than treating # their quotes as shell syntax or permitting escaped prescriptions to go unseen. for extension in yaml yml; do