Skip to main content

create-evenfire-plugin

Build an Evenfire plugin end to end: a WorkflowRecipe with a sandbox web UI, a credentialed backend, an optional MCP server and database, webhooks, OAuth, or an agentic/snippet workflow. Use when the task is to design, scaffold, write, change, containerize, or wire a plugin or recipe for a Clerum/Evenfire cluster, or when you need the exact WorkflowRecipe fields, enums, validation rules, namespace/credential boundaries, image requirements, or the build-and-deploy loop.

Informações da origem

Repositório
evenfire-ai/evenfire-skills
Última atividade na origem
12 de agosto de 2026 às 01:08
Idioma detectado do SKILL.md
inglês
Estrelas
1
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
create-evenfire-plugin
description
Build an Evenfire plugin end to end: a WorkflowRecipe with a sandbox web UI, a credentialed backend, an optional MCP server and database, webhooks, OAuth, or an agentic/snippet workflow. Use when the task is to design, scaffold, write, change, containerize, or wire a plugin or recipe for a Clerum/Evenfire cluster, or when you need the exact WorkflowRecipe fields, enums, validation rules, namespace/credential boundaries, image requirements, or the build-and-deploy loop.
# Building an Evenfire plugin (WorkflowRecipe) > **Verified against Evenfire `dev` at commit `21d9a7d5d` (2026-08-11).** > `git describe`: `v0.3.0-915-g21d9a7d5d`. CRD API `clerum.io/v1alpha1`, > `clerum-crds` chart `0.7.0`. Field names, enums, endpoints, and commands below > match that revision. See [VERSIONS.md](../VERSIONS.md). A plugin is one Kubernetes custom resource, `apiVersion: clerum.io/v1alpha1`, `kind: WorkflowRecipe`. The platform reconciles it into Deployments, StatefulSets, CronJobs, MCP servers, a sandboxed web UI, NetworkPolicies, and workflow runs. A UI plugin is normally a small multi-service repo (`ui/`, `api/`, `mcp/`, `recipe/`) whose recipe wires those images together. Everything in this skill is verified against the live platform code (`clerum.io` CRDs, `workflow-recipes`, `host-context-controller`, `control-api`, `rpc-proxy`, `mcp-host`) and two shipped plugins. Never invent a field, enum, tool, or endpoint that is not listed here. When you need a value that is not here, read the CRD at `charts/clerum-crds/crds/workflowrecipe.yaml` and the WRC reconciler rather than guessing. To publish and install what you build, use the `publish-evenfire-plugin` skill. To run a cluster and debug a deployed recipe, use the `run-debug-evenfire` skill. ## 1. Pick the archetype first One field decides the plugin's whole lifecycle: `isWorkflow = spec.steps.length > 0`. | Archetype | Shape | "Healthy" means | Typical use | |---|---|---|---| | Pure workloads | `workloads`, no `steps` | every workload Deployment/StatefulSet applied and its pods Ready | a UI app + backend + DB, or a standalone MCP server | | Pure workflow | `steps`, no long-running `workloads` | the run reaches `completed` | agentic or snippet pipelines, cron jobs | | Hybrid | both | workloads Ready and runs succeed | a UI app that also emits desktop notifications, a workflow with a sidecar | A recipe MUST populate at least one of `workloads` or `steps` (rule R2). Deploy behavior differs by archetype and this bites people (section 9): a recipe **with** `steps` does not re-apply workload images once it is `active`, a recipe **without** `steps` does. Adding even a dormant keepalive step (as the notifications pattern below does) flips a UI plugin into the "does not auto-propagate image edits" class. ## 2. The credential boundary (the core mental model) The platform routes each workload to one of three namespaces by rule, and you never set `metadata.namespace` yourself: - workload has `transport` -> `mcp-server` - workload is the one named by `spec.ui.workloadRef` -> `sandbox-ui` - every other workload -> `sandbox-recipes` - the WorkflowRecipe object itself always lives in `sandbox-recipes` `sandbox-ui` is the credential-free browser-facing zone. `sandbox-recipes` is where Secrets live. This gives the canonical UI plugin its shape: ``` browser (Electron WebContentsView) | rpc-proxy strips Cookie/Authorization/x-clerum-*, injects x-clerum-user v ui (sandbox-ui) nginx-unprivileged, no Secrets, serves the SPA, | reverse-proxies /api/* to the backend v spec.ui.egress.internal / spec.bindings (default-deny NetworkPolicy) api (sandbox-recipes) holds Secrets via envSecret, talks to db + third parties | db (sandbox-recipes) StatefulSet + PVC mcp (mcp-server) optional MCP server, same dataset, for the chat agent ``` Hard rules that follow from this: - The UI workload MUST NOT declare `envSecret`, `imagePullSecrets`, or any credential reference. There is no Secret-resolution path in `sandbox-ui`; a UI pod that references a Secret sits in `CreateContainerConfigError` forever. - Credentialed code lives on a sibling backend in `sandbox-recipes`, reached from the UI over `spec.ui.egress.internal[]` (the UI's nginx `proxy_pass`). - The backend trusts identity ONLY from the rpc-proxy-injected `X-Clerum-User` header. rpc-proxy strips every client-supplied `x-clerum-*`, `Cookie`, and `Authorization` header, so the embed cannot spoof a user. Never read a user id from the request body or from embed JS. - A Secret is read from the namespace of the workload that consumes it, and nothing copies Secrets across namespaces. If both the backend (`sandbox-recipes`) and the MCP server (`mcp-server`) read the same Secret, it must exist, with the same value, in BOTH namespaces. ## 3. The repo scaffold (multi-service UI plugin) ``` my-plugin/ api/ Dockerfile package.json tsconfig.json src/ migrations/ -> sandbox-recipes ui/ Dockerfile package.json vite.config.ts src/ nginx/ -> sandbox-ui mcp/ Dockerfile package.json src/ -> mcp-server (optional) recipe/ my-plugin.yaml my-plugin.json the WorkflowRecipe docker-compose.dev.yml full local stack, no cluster needed Makefile build-multiarch / dev / recipe-json / publish README.md ``` ### 3.1 The three Dockerfiles (exact, verified shapes) Ports are fixed by convention and must match the recipe: **ui 8080, api 8080, mcp 3000**. All images run non-root with a numeric `USER` so `runAsNonRoot` can verify them, and must be multi-arch (`linux/amd64` + `linux/arm64`). `ui/Dockerfile`, static SPA served by unprivileged nginx. Build the bundle once on the native builder to avoid emulating esbuild/rollup under QEMU: ```dockerfile FROM --platform=$BUILDPLATFORM node:20-alpine AS build WORKDIR /app ARG APP_VERSION=dev ENV APP_VERSION=${APP_VERSION} COPY package.json package-lock.json ./ RUN npm ci --no-audit --no-fund COPY . . RUN npm run build # -> /app/dist, no inline scripts FROM nginxinc/nginx-unprivileged:1.27-alpine ENV API_HOST=api ENV API_PORT=8080 COPY --from=build /app/dist /usr/share/nginx/html COPY nginx/default.conf.template /etc/nginx/templates/default.conf.template USER 101 EXPOSE 8080 ``` `ui/nginx/default.conf.template` (envsubst fills `API_HOST`/`API_PORT`): ```nginx server { listen 8080; root /usr/share/nginx/html; location /api/ { proxy_pass http://${API_HOST}:${API_PORT}; proxy_set_header X-Clerum-User $http_x_clerum_user; } location / { try_files $uri /index.html; } } ``` `api/Dockerfile`, credentialed backend: ```dockerfile FROM node:20-alpine AS build WORKDIR /app COPY package.json ./ RUN npm install COPY tsconfig.json ./ COPY src ./src RUN npm run build && npm prune --omit=dev FROM node:20-alpine AS runtime WORKDIR /app ARG APP_VERSION=dev ENV APP_VERSION=${APP_VERSION} ENV NODE_ENV=production COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/dist ./dist COPY package.json ./ USER 1000 EXPOSE 8080 CMD ["node", "dist/server.js"] ``` `mcp/Dockerfile` (only if the plugin exposes an MCP server): identical to the api runtime but `EXPOSE 3000`. See the `create-evenfire-mcp-server` skill for the server code contract. ### 3.2 Version stamping Thread one `APP_VERSION` build-arg through: - api: `ARG APP_VERSION` -> `ENV APP_VERSION` -> `process.env.APP_VERSION ?? 'dev'` reported by `/api/healthz`. - ui: `ARG APP_VERSION` -> `ENV APP_VERSION` -> Vite `define: { __APP_VERSION__: JSON.stringify(process.env.APP_VERSION || 'dev') }`, shown in the app shell. Do not stamp the mcp image with a build-time version unless it actually reads one; MCP servers version through `registry.json`/`package.json`. ### 3.3 Local dev loop (no cluster) `docker-compose.dev.yml` runs db + api + mcp + ui together. Per-service watch mode also works: `cd api && npm run dev` (tsx watch), `cd ui && npm run dev` (Vite dev server that proxies `/api` to the local api and injects a dev `X-Clerum-User`). Get the plugin fully working here before touching a cluster. ### 3.4 Multi-arch build and push ```bash docker buildx create --use --name multiarch # one-time for svc in api ui mcp; do docker buildx build --platform linux/amd64,linux/arm64 \ --build-arg APP_VERSION="$VERSION" \ --label org.opencontainers.image.source=https://github.com/<org>/<repo> \ --label org.opencontainers.image.licenses=Apache-2.0 \ -t <registry>/<name>-$svc:$VERSION --push "$svc/" done ``` `<registry>` is a public registry (Docker Hub, GHCR, Quay) for a public plugin, or `registry.evenfire.ai/<org>` for an org-private one. See `publish-evenfire-plugin`. ## 4. The recipe YAML `recipe/my-plugin.yaml` wires the images. Full UI + backend + DB + MCP example: ```yaml apiVersion: clerum.io/v1alpha1 kind: WorkflowRecipe metadata: name: my-plugin # DNS-1123, <= 63 chars. Do NOT set metadata.namespace. spec: description: | One-sentence summary first (operators see it in the catalog list). Then document EVERY Secret an operator must create before install: its name, keys, which namespace(s), and where each value comes from. security: isolationLevel: minimal # minimal | standard | strict workloads: - id: db type: statefulset image: postgres:16-alpine port: 5432 env: - { name: POSTGRES_USER, value: myplugin } - { name: POSTGRES_DB, value: myplugin } - { name: PGDATA, value: /var/lib/postgresql/data/pgdata } # subdir, not the mount root envSecret: name: my-plugin-secrets keys: - { secretKey: pg-password, envVar: POSTGRES_PASSWORD } volumeMounts: - { name: pgdata, mountPath: /var/lib/postgresql/data } volumeClaimTemplates: - { name: pgdata, storageClass: standard, accessMode: ReadWriteOnce, size: 1Gi } security: runAsUser: 70 # postgres:16-alpine runs as uid 70 runAsGroup: 70 fsGroup: 70 addCapabilities: [CHOWN, FOWNER, DAC_OVERRIDE] # initdb needs these - id: api type: deployment image: <registry>/my-plugin-api:1.0.0 port: 8080 envSecret: name: my-plugin-secrets keys: - { secretKey: pg-password, envVar: DATABASE_PASSWORD } healthCheck: { type: http, path: /api/healthz, port: 8080 } egressBindings: - { dns: api.anthropic.com, port: 443 } # every third-party host, exact-host dependsOn: [db] - id: ui type: deployment image: <registry>/my-plugin-ui:1.0.0 port: 8080 env: - { name: API_HOST, value: '{{api:host}}' } # template var, resolved at build time - { name: API_PORT, value: '{{api:port}}' } healthCheck: { type: http, path: /, port: 8080 } - id: mcp # optional MCP server, same dataset type: deployment image: <registry>/my-plugin-mcp:1.0.0 port: 3000 transport: { type: streamableHttp, path: /mcp } envSecret: name: my-plugin-secrets # note: this Secret must ALSO exist in mcp-server ns keys: - { secretKey: mcp-api-token, envVar: MCP_API_TOKEN } ui: workloadRef: ui # must be a deployment, replicas 1, no transport (R15/R16) port: 8080 # must be 8080 (rpc-proxy allow-list), else 502 at view time title: My Plugin defaultPath: / egress: internal: - { workloadRef: api, port: 8080 } # opens ui -> api (nginx proxy_pass target) bindings: - { from: mcp, to: api, port: 8080 } # mcp (mcp-server ns) -> api (sandbox-recipes) ``` ### 4.1 Field and enum reference (copy values verbatim) - `workloads[].type`: `deployment | statefulset | cronjob | job | daemonset`. A `cronjob` workload MUST set the workload-level `schedule`. Max 25 workloads. - `workloads[].type` required trio: `id`, `type`, `image`. Common optionals: `port`, `replicas` (0-20), `command`, `args`, `env`, `envSecret`, `volumeMounts`, `volumeClaimTemplates`, `resources`, `healthCheck`, `dependsOn`, `imagePullSecrets`, `oauthClientRefs`, `egressBindings`, `includeWhen`, `transport`, `security`, `schedule`. - `env[]` items are `{name, value}` strings only. There is **no** `valueFrom`, `envFrom`, or `envFromConfigMap`. The only Secret-to-env path is `envSecret` (`name` + `keys[{secretKey, envVar, optional?}]`). - `healthCheck`: `{type: http|tcp|exec, path, port, command[], initialDelaySeconds, periodSeconds, timeoutSeconds, failureThreshold}`. There is **no** `startupProbe` and **no** `successThreshold`; the one `healthCheck` block serves both liveness and readiness. For slow starts, widen `initialDelaySeconds`/`failureThreshold`, or return 503 from the health path until ready. - `security` overrides: `runAsUser`/`runAsGroup`/`fsGroup` (each min 1, so root uid 0 is rejected), `addCapabilities` (only `CHOWN|FOWNER|DAC_OVERRIDE|NET_BIND_SERVICE`, enforced twice), `prepareVolumeOwnership`. Setting `runAsUser` forces `runAsNonRoot: true`. - `transport.type`: `streamableHttp | sse | stdio` (author `streamableHttp` for own-code HTTP servers). `path` defaults matter, set `/mcp`. - `egressBindings[]`: `{egressClass: exact-host|public-web, dns, port, protocol}`. `exact-host` (default) opens one DNS host + port. `public-web` opens public TCP 80/443 only and must NOT carry `dns`/`port`. Max 20. - `volumeClaimTemplates[]`: `{name, storageClass, accessMode, size}`, `accessMode` is `ReadWriteOnce|ReadOnlyMany|ReadWriteMany`. Use `standard` for minikube, the cluster's real class for GKE (`standard-rwo`) or DO (`do-block-storage`). Max 4. - `security.isolationLevel`: `minimal | standard | strict`, additive, default `minimal` (root allowed, writable root FS, all caps dropped, seccomp RuntimeDefault). `standard` adds `runAsNonRoot` + read-only root FS. `strict` adds pod identity pinned to 65534 + `automountServiceAccountToken: false` + the PodSecurity `restricted` label. Prefer `minimal` and use a per-workload `security.runAsUser` when a specific non-root uid is needed. - `spec.ui`: `workloadRef` + `port` required; optional `title` (<=100 chars),
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub