Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
Expand Down
2 changes: 1 addition & 1 deletion .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
Expand Down
3 changes: 2 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Binary file added plugins/ksail/.claude-plugin/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion plugins/ksail/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
51 changes: 51 additions & 0 deletions plugins/ksail/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Comment thread
devantler marked this conversation as resolved.
Resolved
"logo": "./.claude-plugin/icon.png"
}
}
11 changes: 11 additions & 0 deletions plugins/ksail/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion plugins/ksail/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
25 changes: 24 additions & 1 deletion scripts/guard-gh-json-fields.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ] ;;
Comment thread
devantler marked this conversation as resolved.
Resolved
*.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
Expand Down Expand Up @@ -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}"
Expand Down
42 changes: 42 additions & 0 deletions scripts/guard-gh-json-fields.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
devantler marked this conversation as resolved.
Resolved
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
Expand Down
Loading