AI agents MUST follow these rules for every commit they author. They override any default Crush commit template.
- No attribution trailers. Do not add
Signed-off-by,Co-authored-by,Assisted-by,Generated-by, or any other trailer attributing work to an AI, tool, or third party. - Subject line: ≤ 50 characters, capitalised, no trailing period, imperative mood (
Add support for X, notAdded/Adds). - Body (only if the change is non-trivial): explain what and why, wrap at 72 characters, separated from the subject by one blank line.
- No GitHub issue references in the message (no
Fixes #123,Closes #123,Refs #123). Put issue links in the PR description instead. - No GitHub @-mentions of users or teams (no
@duynhlab,@platform-team).
Acceptable example:
Add Kyverno admission policies for PSS baseline
Roll out Tier 1 ClusterPolicies in Audit mode so we can observe the
policy reports for one week before flipping to Enforce. Operators that
legitimately violate baseline are whitelisted via PolicyException with
owner and TTL annotations.
- NEVER push directly to
main. No exceptions. All changes go through a feature branch and PR. Never rungit push origin mainfrom a local checkout. - Create a branch with a conventional prefix before any work:
feat/<short-desc>— new feature or capabilityfix/<short-desc>— bug fixchore/<short-desc>— tooling, deps, refactor with no behaviour changedocs/<short-desc>— documentation onlyrefactor/<short-desc>— code restructure, no behaviour changeci/<short-desc>— CI/CD pipeline changes
- One logical change per branch. Keep branches short-lived.
- Push the branch (
git push -u origin <branch>), then open a PR againstmain. - Squash-merge via PR.
A hyper-mcp WebAssembly plugin written in Go and compiled with TinyGo to wasip1. Exposes two MCP tools — base64_encode and base64_decode — backed by stdlib encoding/base64. Both tools accept input: string and an optional url_safe: bool (RFC 4648 §5 URL-safe alphabet).
main.go <- Plugin handler implementations. The only file you normally edit.
exports.go <- Generated WASM `//export` wrappers. Do not edit.
imports.go <- Generated host-function bindings. Do not edit.
types.go <- 1600+ lines of MCP protocol types. Do not edit; reference only.
test/ <- Standalone Go host harness (separate go.mod) that loads plugin.wasm via
github.com/extism/go-sdk and exercises both tools end-to-end.
Data flow: hyper-mcp host invokes a //export call_tool (etc.) in exports.go → wrapper JSON-decodes input via pdk.InputJSON → calls the same-named handler in main.go → JSON-encodes output via pdk.OutputJSON. The plugin runs as an Extism WASM module; there is no HTTP server, no stdin/stdout parsing.
func main() {} in main.go:95 is intentionally empty — do not remove it. TinyGo requires it as the compile entrypoint, but actual entrypoints are the //export ... functions.
Local build (no TinyGo installed) — run the official TinyGo image via podman. Use --userns=keep-id so the output file is owned by the host user, and GOFLAGS=-buildvcs=false to skip VCS stamping (the container lacks git context for the bind mount):
podman run --rm --userns=keep-id -e GOFLAGS=-buildvcs=false -e HOME=/tmp \
-v "$PWD":/src:Z -w /src docker.io/tinygo/tinygo:0.40.1 \
tinygo build -target=wasip1 -no-debug -panic=trap -scheduler=none -o plugin.wasmCI uses the host-installed equivalent:
GOOS=wasip1 GOARCH=wasm tinygo build -no-debug -panic=trap -scheduler=none -o plugin.wasmTest the built plugin:
cd test && go run .Produces plugin.wasm (~425KB). The harness exercises encode/decode (std + URL-safe), invalid input, unknown tool, and missing-argument paths.
Build OCI image with podman:
podman build -t base64-plugin:latest .The Dockerfile is FROM scratch + COPY plugin.wasm — it does not compile; plugin.wasm must exist first.
- Requires TinyGo 0.40.1 (pinned in CI and in the podman command above). Stock
go buildwill fail withmissing function bodyerrors on extism's PDK — those functions only have bodies for the wasip1+TinyGo build path. Always build via TinyGo. -scheduler=nonemeans goroutines are forbidden at runtime. Nogo func(), channel-based sync,time.Sleep-based concurrency, etc.-panic=trapmeans panics produce an opaque WASM trap with no stack trace. Always return errors (orIsError: trueCallToolResults); never panic.- The host harness in
test/is a separate Go module so it can use the native extism SDK without conflicting with the wasip1 build of the plugin.
go.modmodule path isgithub.com/duynhlab/base64-plugin. Nothing inside this repo imports the module (the plugin is built as a wasip1 binary with//exportentry points), so the path is essentially cosmetic — keep it in sync with the GitHub repo name on any future rename.- gopls reports
_CallTool,_ListTools, etc. as unused — false positive. They are kept alive by//exportdirectives that gopls (running with host Go) doesn't see. Do not delete. CallToolResult.Contentis[]ContentBlock(a tagged-union struct), not[]json.RawMessage. Build a text result withContentBlock{Text: &TextContent{Text: "..."}}. Earlier versions of the upstream README show raw JSON — that's outdated for the currenttypes.go.Tool.InputSchemaisjsonschema.Schemafromgithub.com/invopop/jsonschema. ItsPropertiesfield is*orderedmap.OrderedMap[string, *jsonschema.Schema]— build it withorderedmap.New[string, *jsonschema.Schema]()and.Set(...), not amap[string]anyliteral.- Tool arguments arrive as
map[string]anyininput.Request.Arguments. Always type-assert with, ok; under-panic=trapa bad assertion is an unrecoverable WASM trap. - For any MCP capability you don't support, return an empty result, not an error — e.g.
GetPromptreturns&GetPromptResult{}, nil. The host will probe these handlers; returningnot implementederrors makes the host surface failures.
.github/workflows/:
ci.yml— build-only on push/PR tomain. No tests.nightly-docker.yml,nightly-oras.yml— scheduled nightly publishes.release-docker.yml,release-oras.yml— triggered onv*tags. Two parallel publishing paths: GHCR Docker image and ORAS-pushed OCI artifact.
All workflows pin TinyGo to 0.40.1 via acifani/setup-tinygo@db56321.... If you bump TinyGo, bump it in all five workflows together.
Local:
{
"plugins": {
"base64": { "url": "file:///abs/path/to/plugin.wasm" }
}
}From the podman-built image (after podman push to a registry):
{
"plugins": {
"base64": { "url": "oci://ghcr.io/duynhlab/base64-plugin:latest" }
}
}