From db8bb7236d1266dd9596dc01cb46e414fd794538 Mon Sep 17 00:00:00 2001 From: alexsong-oai Date: Mon, 23 Mar 2026 19:08:30 -0700 Subject: [PATCH] Add plugin-creator as system skill (#15554) --- .../assets/samples/plugin-creator/SKILL.md | 148 +++++++++ .../samples/plugin-creator/agents/openai.yaml | 6 + .../assets/plugin-creator-small.svg | 3 + .../plugin-creator/assets/plugin-creator.png | Bin 0 -> 1563 bytes .../references/plugin-json-spec.md | 166 ++++++++++ .../scripts/create_basic_plugin.py | 291 ++++++++++++++++++ 6 files changed, 614 insertions(+) create mode 100644 codex-rs/skills/src/assets/samples/plugin-creator/SKILL.md create mode 100644 codex-rs/skills/src/assets/samples/plugin-creator/agents/openai.yaml create mode 100644 codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator-small.svg create mode 100644 codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator.png create mode 100644 codex-rs/skills/src/assets/samples/plugin-creator/references/plugin-json-spec.md create mode 100755 codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/SKILL.md b/codex-rs/skills/src/assets/samples/plugin-creator/SKILL.md new file mode 100644 index 000000000..28d47b754 --- /dev/null +++ b/codex-rs/skills/src/assets/samples/plugin-creator/SKILL.md @@ -0,0 +1,148 @@ +--- +name: plugin-creator +description: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, and baseline placeholders you can edit before publishing or testing. Use when Codex needs to create a new local plugin, add optional plugin structure, or generate or update repo-root `.agents/plugins/marketplace.json` entries for plugin ordering and availability metadata. +--- + +# Plugin Creator + +## Quick Start + +1. Run the scaffold script: + +```bash + # Plugin names are normalized to lower-case hyphen-case and must be <= 64 chars. + # The generated folder and plugin.json name are always the same. +# Run from repo root (or replace .agents/... with the absolute path to this SKILL). +# By default creates in /plugins/. +python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py +``` + +2. Open `/.codex-plugin/plugin.json` and replace `[TODO: ...]` placeholders. + +3. Generate or update the repo marketplace entry when the plugin should appear in Codex UI ordering: + +```bash +# marketplace.json always lives at /.agents/plugins/marketplace.json +python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --with-marketplace +``` + +4. Generate/adjust optional companion folders as needed: + +```bash +python3 .agents/skills/plugin-creator/scripts/create_basic_plugin.py my-plugin --path \ + --with-skills --with-hooks --with-scripts --with-assets --with-mcp --with-apps --with-marketplace +``` + +`` is the directory where the plugin folder `` will be created (for example `~/code/plugins`). + +## What this skill creates + +- Creates plugin root at `///`. +- Always creates `///.codex-plugin/plugin.json`. +- Fills the manifest with the full schema shape, placeholder values, and the complete `interface` section. +- Creates or updates `/.agents/plugins/marketplace.json` when `--with-marketplace` is set. + - If the marketplace file does not exist yet, seed top-level `name` plus `interface.displayName` placeholders before adding the first plugin entry. +- `` is normalized using skill-creator naming rules: + - `My Plugin` → `my-plugin` + - `My--Plugin` → `my-plugin` + - underscores, spaces, and punctuation are converted to `-` + - result is lower-case hyphen-delimited with consecutive hyphens collapsed +- Supports optional creation of: + - `skills/` + - `hooks/` + - `scripts/` + - `assets/` + - `.mcp.json` + - `.app.json` + +## Marketplace workflow + +- `marketplace.json` always lives at `/.agents/plugins/marketplace.json`. +- Marketplace root metadata supports top-level `name` plus optional `interface.displayName`. +- Treat plugin order in `plugins[]` as render order in Codex. Append new entries unless a user explicitly asks to reorder the list. +- `displayName` belongs inside the marketplace `interface` object, not individual `plugins[]` entries. +- Each generated marketplace entry must include all of: + - `policy.installation` + - `policy.authentication` + - `category` +- Default new entries to: + - `policy.installation: "AVAILABLE"` + - `policy.authentication: "ON_INSTALL"` +- Override defaults only when the user explicitly specifies another allowed value. +- Allowed `policy.installation` values: + - `NOT_AVAILABLE` + - `AVAILABLE` + - `INSTALLED_BY_DEFAULT` +- Allowed `policy.authentication` values: + - `ON_INSTALL` + - `ON_USE` +- Treat `policy.products` as an override. Omit it unless the user explicitly requests product gating. +- The generated plugin entry shape is: + +```json +{ + "name": "plugin-name", + "source": { + "source": "local", + "path": "./plugins/plugin-name" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" +} +``` + +- Use `--force` only when intentionally replacing an existing marketplace entry for the same plugin name. +- If `/.agents/plugins/marketplace.json` does not exist yet, create it with top-level `"name"`, an `"interface"` object containing `"displayName"`, and a `plugins` array, then add the new entry. + +- For a brand-new marketplace file, the root object should look like: + +```json +{ + "name": "[TODO: marketplace-name]", + "interface": { + "displayName": "[TODO: Marketplace Display Name]" + }, + "plugins": [ + { + "name": "plugin-name", + "source": { + "source": "local", + "path": "./plugins/plugin-name" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} +``` + +## Required behavior + +- Outer folder name and `plugin.json` `"name"` are always the same normalized plugin name. +- Do not remove required structure; keep `.codex-plugin/plugin.json` present. +- Keep manifest values as placeholders until a human or follow-up step explicitly fills them. +- If creating files inside an existing plugin path, use `--force` only when overwrite is intentional. +- Preserve any existing marketplace `interface.displayName`. +- When generating marketplace entries, always write `policy.installation`, `policy.authentication`, and `category` even if their values are defaults. +- Add `policy.products` only when the user explicitly asks for that override. +- Keep marketplace `source.path` relative to repo root as `./plugins/`. + +## Reference to exact spec sample + +For the exact canonical sample JSON for both plugin manifests and marketplace entries, use: + +- `references/plugin-json-spec.md` + +## Validation + +After editing `SKILL.md`, run: + +```bash +python3 /scripts/quick_validate.py .agents/skills/plugin-creator +``` diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/agents/openai.yaml b/codex-rs/skills/src/assets/samples/plugin-creator/agents/openai.yaml new file mode 100644 index 000000000..979b49859 --- /dev/null +++ b/codex-rs/skills/src/assets/samples/plugin-creator/agents/openai.yaml @@ -0,0 +1,6 @@ +interface: + display_name: "Plugin Creator" + short_description: "Scaffold plugins and marketplace entries" + default_prompt: "Use $plugin-creator to scaffold a plugin with placeholder plugin.json, optional structure, and a marketplace.json entry." + icon_small: "./assets/plugin-creator-small.svg" + icon_large: "./assets/plugin-creator.png" diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator-small.svg b/codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator-small.svg new file mode 100644 index 000000000..c6e4f67c6 --- /dev/null +++ b/codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator.png b/codex-rs/skills/src/assets/samples/plugin-creator/assets/plugin-creator.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3d6d82fa78fbdce97af3c17f6a25c683aa3290 GIT binary patch literal 1563 zcmV+$2ITpPP)1#i4F!Dtb@FnH%p6OalPi4TDy zMcTdqNqhk%K`*Qbx>_X{K4hvjHd<=hH0fq{Gsiib9J_g*IrFfyGygA?&1Rpl>Iw2qhkb ze|)YB#{A1DHe~H{=}oVwFPPA*37~O^y*^+fpb`thKY1$)zGE1LrVQMsu}jdkKwuIF z!Y>Y|aerZBkl8tR2W{Vc1+7>_fXyPMKjmfh7264^#1Q8f8yfM)Gsj@1QV6g#maH@I zP%I~8Ek1&*Sz<4`O%n=C#xm?`ka z@f*eCnG*Imx=o;)FU8GPEG9%?fE(jGa4*wlOu)drquT{LDa8yFs(BEJAz$>R*gPVE zqU#+{sHQ>IV#p$cqEO9(Y=$ALItt^-S@v#+A*(J*LO`5i$f|}?GICI;ra;_c$ihcq zX6#?7t76DPMwx(OH38xkLlzp!uBppVEb<_JVaNhUVVGdAj!ZzYNQ3x|AuFRSya%H+ z?dbZhQ0jjifQKRr5)?-HzW@Ca#_cg2y7h{`C>$TMzDr80D_JB#g2uq?7v-;0HimyF z%!H*6!ecrjR#+G^-I_yvejNAeo`a<|YBMrB2#6*u;OYX9V;b-SyY zmSuIfx3`<{v8qoM4|80YXzgSg7L69b@21RF+`z))FOtR$uxAc?!1W*3JruXoD~WPVK^^v5W;X* zpdf_dzQ8~TL#;qS2tz&JK?uWJzP@9~zdK179`pAt+{P25gyAjUK={;^Ves}2U?==6 zNf;tu_96V2E4^TR<-qwx`(A%qKJ|LP3BwTiGa(EWhXUIZ!jHo1q$CkT~d#9UdUR zH40%o$QuZQi$i}mMj?#VDT6HUKah5JtJNrku|D+)DV1De$d*y~ogWBegVa9;v@4x` z{E^m^dv>Y3)-b^IHelgvz;bpZlf!BnL!=DS*wpgU)oZ_Yc0Tbalu)#Ku-%=0qSQQw zNPQo|`ICR&zKgduf@(L>gD6zp1DQ&W{*=*XftU-Bx z{{AAaUi%GI*nGtbL!>kzrBZ3#>yIi-O6)L1N+5D`wYCsZVu>MA1<7o7SqLbx#Skfh zh!xgWmj{RvYYdUEo2<$LK#4tu$n`2~?FwtF{91jn{I@yA@4gUs43X;z*=U8eRh}nB zZ@!E}7h*pdGS?vX=F0UC71dDKEG$029y`&?4tIz02f3n9Xus>kIZ7E~2%2`79eMr( zBrMmT%M$w#UDy9A6bf}v=-|G+IQzzn<BbTJjk&~1^x!BXwGidKmHbOD6^2FPh=j>?` zzdy/.agents/plugins/marketplace.json` +- Local plugin: `~/.agents/plugins/marketplace.json` + +```json +{ + "name": "openai-curated", + "interface": { + "displayName": "ChatGPT Official" + }, + "plugins": [ + { + "name": "linear", + "source": { + "source": "local", + "path": "./plugins/linear" + }, + "installPolicy": "AVAILABLE", + "authPolicy": "ON_INSTALL", + "category": "Productivity" + } + ] +} +``` + +## Marketplace field guide + +### Top-level fields + +- `name` (`string`): Marketplace identifier or catalog name. +- `interface` (`object`, optional): Marketplace presentation metadata. +- `plugins` (`array`): Ordered plugin entries. This order determines how Codex renders plugins. + +### `interface` fields + +- `displayName` (`string`, optional): User-facing marketplace title. + +### Plugin entry fields + +- `name` (`string`): Plugin identifier. Match the plugin folder name and `plugin.json` `name`. +- `source` (`object`): Plugin source descriptor. + - `source` (`string`): Use `local` for this repo workflow. + - `path` (`string`): Relative plugin path based on the marketplace root. + - Repo plugin: `./plugins/` + - Local plugin in `~/.agents/plugins/marketplace.json`: `./.codex/plugins/` +- `policy` (`object`): Marketplace policy block. Always include it. + - `installation` (`string`): Availability policy. + - Allowed values: `NOT_AVAILABLE`, `AVAILABLE`, `INSTALLED_BY_DEFAULT` + - Default for new entries: `AVAILABLE` + - `authentication` (`string`): Authentication timing policy. + - Allowed values: `ON_INSTALL`, `ON_USE` + - Default for new entries: `ON_INSTALL` + - `products` (`array` of `string`, optional): Product override for this plugin entry. Omit it unless product gating is explicitly requested. +- `category` (`string`): Display category bucket. Always include it. + +### Marketplace generation rules + +- `displayName` belongs under the top-level `interface` object, not individual plugin entries. +- When creating a new marketplace file from scratch, seed `interface.displayName` alongside top-level `name`. +- Always include `policy.installation`, `policy.authentication`, and `category` on every generated or updated plugin entry. +- Treat `policy.products` as an override and omit it unless explicitly requested. +- Append new entries unless the user explicitly requests reordering. +- Replace an existing entry for the same plugin only when overwrite is intentional. +- Choose marketplace location to match the plugin destination: + - Repo plugin: `/.agents/plugins/marketplace.json` + - Local plugin: `~/.agents/plugins/marketplace.json` diff --git a/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py new file mode 100755 index 000000000..5a6109316 --- /dev/null +++ b/codex-rs/skills/src/assets/samples/plugin-creator/scripts/create_basic_plugin.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +"""Scaffold a plugin directory and optionally update marketplace.json.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + + +MAX_PLUGIN_NAME_LENGTH = 64 +DEFAULT_PLUGIN_PARENT = Path.cwd() / "plugins" +DEFAULT_MARKETPLACE_PATH = Path.cwd() / ".agents" / "plugins" / "marketplace.json" +DEFAULT_INSTALL_POLICY = "AVAILABLE" +DEFAULT_AUTH_POLICY = "ON_INSTALL" +DEFAULT_CATEGORY = "Productivity" +DEFAULT_MARKETPLACE_DISPLAY_NAME = "[TODO: Marketplace Display Name]" +VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"} +VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"} + + +def normalize_plugin_name(plugin_name: str) -> str: + """Normalize a plugin name to lowercase hyphen-case.""" + normalized = plugin_name.strip().lower() + normalized = re.sub(r"[^a-z0-9]+", "-", normalized) + normalized = normalized.strip("-") + normalized = re.sub(r"-{2,}", "-", normalized) + return normalized + + +def validate_plugin_name(plugin_name: str) -> None: + if not plugin_name: + raise ValueError("Plugin name must include at least one letter or digit.") + if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH: + raise ValueError( + f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). " + f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters." + ) + + +def build_plugin_json(plugin_name: str) -> dict: + return { + "name": plugin_name, + "version": "[TODO: 1.2.0]", + "description": "[TODO: Brief plugin description]", + "author": { + "name": "[TODO: Author Name]", + "email": "[TODO: author@example.com]", + "url": "[TODO: https://github.com/author]", + }, + "homepage": "[TODO: https://docs.example.com/plugin]", + "repository": "[TODO: https://github.com/author/plugin]", + "license": "[TODO: MIT]", + "keywords": ["[TODO: keyword1]", "[TODO: keyword2]"], + "skills": "[TODO: ./skills/]", + "hooks": "[TODO: ./hooks.json]", + "mcpServers": "[TODO: ./.mcp.json]", + "apps": "[TODO: ./.app.json]", + "interface": { + "displayName": "[TODO: Plugin Display Name]", + "shortDescription": "[TODO: Short description for subtitle]", + "longDescription": "[TODO: Long description for details page]", + "developerName": "[TODO: OpenAI]", + "category": "[TODO: Productivity]", + "capabilities": ["[TODO: Interactive]", "[TODO: Write]"], + "websiteURL": "[TODO: https://openai.com/]", + "privacyPolicyURL": "[TODO: https://openai.com/policies/row-privacy-policy/]", + "termsOfServiceURL": "[TODO: https://openai.com/policies/row-terms-of-use/]", + "defaultPrompt": [ + "[TODO: Summarize my inbox and draft replies for me.]", + "[TODO: Find open bugs and turn them into tickets.]", + "[TODO: Review today's meetings and flag gaps.]", + ], + "brandColor": "[TODO: #3B82F6]", + "composerIcon": "[TODO: ./assets/icon.png]", + "logo": "[TODO: ./assets/logo.png]", + "screenshots": [ + "[TODO: ./assets/screenshot1.png]", + "[TODO: ./assets/screenshot2.png]", + "[TODO: ./assets/screenshot3.png]", + ], + }, + } + + +def build_marketplace_entry( + plugin_name: str, + install_policy: str, + auth_policy: str, + category: str, +) -> dict[str, Any]: + return { + "name": plugin_name, + "source": { + "source": "local", + "path": f"./plugins/{plugin_name}", + }, + "policy": { + "installation": install_policy, + "authentication": auth_policy, + }, + "category": category, + } + + +def load_json(path: Path) -> dict[str, Any]: + with path.open() as handle: + return json.load(handle) + + +def build_default_marketplace() -> dict[str, Any]: + return { + "name": "[TODO: marketplace-name]", + "interface": { + "displayName": DEFAULT_MARKETPLACE_DISPLAY_NAME, + }, + "plugins": [], + } + + +def validate_marketplace_interface(payload: dict[str, Any]) -> None: + interface = payload.get("interface") + if interface is not None and not isinstance(interface, dict): + raise ValueError("marketplace.json field 'interface' must be an object.") + + +def update_marketplace_json( + marketplace_path: Path, + plugin_name: str, + install_policy: str, + auth_policy: str, + category: str, + force: bool, +) -> None: + if marketplace_path.exists(): + payload = load_json(marketplace_path) + else: + payload = build_default_marketplace() + + if not isinstance(payload, dict): + raise ValueError(f"{marketplace_path} must contain a JSON object.") + + validate_marketplace_interface(payload) + + plugins = payload.setdefault("plugins", []) + if not isinstance(plugins, list): + raise ValueError(f"{marketplace_path} field 'plugins' must be an array.") + + new_entry = build_marketplace_entry(plugin_name, install_policy, auth_policy, category) + + for index, entry in enumerate(plugins): + if isinstance(entry, dict) and entry.get("name") == plugin_name: + if not force: + raise FileExistsError( + f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. " + "Use --force to overwrite that entry." + ) + plugins[index] = new_entry + break + else: + plugins.append(new_entry) + + write_json(marketplace_path, payload, force=True) + + +def write_json(path: Path, data: dict, force: bool) -> None: + if path.exists() and not force: + raise FileExistsError(f"{path} already exists. Use --force to overwrite.") + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as handle: + json.dump(data, handle, indent=2) + handle.write("\n") + + +def create_stub_file(path: Path, payload: dict, force: bool) -> None: + if path.exists() and not force: + return + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as handle: + json.dump(payload, handle, indent=2) + handle.write("\n") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Create a plugin skeleton with placeholder plugin.json." + ) + parser.add_argument("plugin_name") + parser.add_argument( + "--path", + default=str(DEFAULT_PLUGIN_PARENT), + help="Parent directory for plugin creation (defaults to /plugins)", + ) + parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory") + parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory") + parser.add_argument("--with-scripts", action="store_true", help="Create scripts/ directory") + parser.add_argument("--with-assets", action="store_true", help="Create assets/ directory") + parser.add_argument("--with-mcp", action="store_true", help="Create .mcp.json placeholder") + parser.add_argument("--with-apps", action="store_true", help="Create .app.json placeholder") + parser.add_argument( + "--with-marketplace", + action="store_true", + help="Create or update /.agents/plugins/marketplace.json", + ) + parser.add_argument( + "--marketplace-path", + default=str(DEFAULT_MARKETPLACE_PATH), + help="Path to marketplace.json (defaults to /.agents/plugins/marketplace.json)", + ) + parser.add_argument( + "--install-policy", + default=DEFAULT_INSTALL_POLICY, + choices=sorted(VALID_INSTALL_POLICIES), + help="Marketplace policy.installation value", + ) + parser.add_argument( + "--auth-policy", + default=DEFAULT_AUTH_POLICY, + choices=sorted(VALID_AUTH_POLICIES), + help="Marketplace policy.authentication value", + ) + parser.add_argument( + "--category", + default=DEFAULT_CATEGORY, + help="Marketplace category value", + ) + parser.add_argument("--force", action="store_true", help="Overwrite existing files") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + raw_plugin_name = args.plugin_name + plugin_name = normalize_plugin_name(raw_plugin_name) + if plugin_name != raw_plugin_name: + print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.") + validate_plugin_name(plugin_name) + + plugin_root = (Path(args.path).expanduser().resolve() / plugin_name) + plugin_root.mkdir(parents=True, exist_ok=True) + + plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json" + write_json(plugin_json_path, build_plugin_json(plugin_name), args.force) + + optional_directories = { + "skills": args.with_skills, + "hooks": args.with_hooks, + "scripts": args.with_scripts, + "assets": args.with_assets, + } + for folder, enabled in optional_directories.items(): + if enabled: + (plugin_root / folder).mkdir(parents=True, exist_ok=True) + + if args.with_mcp: + create_stub_file( + plugin_root / ".mcp.json", + {"mcpServers": {}}, + args.force, + ) + + if args.with_apps: + create_stub_file( + plugin_root / ".app.json", + { + "apps": {}, + }, + args.force, + ) + + if args.with_marketplace: + marketplace_path = Path(args.marketplace_path).expanduser().resolve() + update_marketplace_json( + marketplace_path, + plugin_name, + args.install_policy, + args.auth_policy, + args.category, + args.force, + ) + + print(f"Created plugin scaffold: {plugin_root}") + print(f"plugin manifest: {plugin_json_path}") + if args.with_marketplace: + print(f"marketplace manifest: {marketplace_path}") + + +if __name__ == "__main__": + main()