fix: correct Codex plugin output for the target registry - #16
Conversation
Re-checking the originally planned Cursor "fixes" against the vendored plugin.schema.json / marketplace.schema.json (this repo's own conformance oracle, already passing against the current shape) showed they were wrong: - displayName/category/tags ARE valid plugin.json fields per the schema (additionalProperties: false, and they're explicitly listed) — not marketplace-entry-only fields as previously assumed. - Marketplace `owner` is genuinely optional (required: ["name", "plugins"] does not include it) — not required as previously assumed. Moving category/tags to the marketplace entry would have been actively wrong: entries only allow name/source/description (additionalProperties: false). None of that is changed here. What this commit actually does: - Migrates cursor onto PluginTargetDefinition, preserving every existing field and behavior (verified by the vendored-schema conformance test staying green). - Fixes one genuine, low-risk issue: the manifest builder's own hardcoded default-components list could diverge from this target's actual defaultComponents (components.ts). Replaced both with one list of schema-valid pointer fields, checked directly against the plugin's real resolved componentDirs — eliminates the divergence risk with no observable behavior change (confirmed via a new test exercising the one case that could have differed: an explicit `components: [...]` override). - Ports update-check's hook-injection into the new shared engine (src/targets/engine.ts), which previously only existed in the legacy emitCursor/emitClaude path — migrating cursor without this would have silently dropped update-check support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildPluginManifest used the target-level `version` param directly instead of `pluginConfig.version ?? version`, silently dropping a per-plugin version override — a real regression from the pre-migration behavior, caught by porting the equivalent test from the claude migration (no test previously covered this for cursor specifically). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports emitClaude/validateClaude into src/targets/claude.ts as a PluginTargetDefinition, verified directly against `claude plugin validate --strict`: a minimal manifest with only `name` fails that check, confirming the existing version/description/author.name requirements already match the real CLI rather than over-constraining it. Applies the per-plugin version override in buildPluginManifest (pluginConfig.version ?? version) up front, matching the identical fix just made to cursor's copy of this pattern.
Ports emitCodex/validateCodex into src/targets/codex.ts as a PluginTargetDefinition, correcting the plugin format's real shape — re-verified directly against developers.openai.com/codex/plugins/build (fetched twice, independently, for consistency) since Codex has no CLI validator or vendored schema to check against: - plugin.json requires only "name"; version/description/author etc. are optional. The previous validator wrongly required version and description. - Every marketplace entry needs policy.installation, policy.authentication, and category — previously unvalidated, so an incomplete entry shipped silently. pluginpack can't infer these, so the base entry stays guess-free and validateOutput now errors clearly when an author never supplies them via the per-plugin `entry` passthrough (already how the existing conformance fixture supplies them). - A marketplace entry's source is a bare string only for local plugins (the only shape pluginpack itself ever emits); url/git-subdir/npm sources are structured objects with an inner "source" discriminator. validateMarketplaceEntry now accepts either shape instead of the previously shared, string-only validator. - plugin.json now declares a `hooks` pointer when hooks/ is present, matching skills/mcpServers (previously only skills/mcpServers were declared, so hooks were emitted but never referenced). Updates CONFORMANCE.md's Codex section, which had pinned a stale, bare-string-only shape from an earlier doc retrieval.
| return null; | ||
| } | ||
| const { name } = entry; | ||
| if (typeof entry.source === "string") { |
There was a problem hiding this comment.
Issue: validateCodexEntry does not return null when entry.source is invalid (unsafe path, missing local directory, or malformed structured source), so validateOutput still attempts to resolve and read the plugin manifest using the invalid source, unlike the legacy validatePluginEntry and shared validateBareStringSourceEntry helpers which bail out on source errors.
Suggested fix: Update validateCodexEntry to mirror validateBareStringSourceEntry and the legacy validatePluginEntry contract by returning null immediately after any source-related error (unsafe relative path, missing local directory, invalid source.source discriminator, missing required fields for local/url/git-subdir/npm), so that callers like codex.validateOutput skip further filesystem validation for that entry.
🔧 Tag @ glean-for-engineering to fix or click here to fix in Glean
💬 Help us improve! Was this comment helpful? React with 👍 or 👎
…-registry # ------------------------ >8 ------------------------ # Do not modify or remove the line above. # Everything below it will be ignored. # # Conflicts: # src/targets/registry.ts # tests/core.test.ts
Summary
Ports
emitCodex/validateCodexintosrc/targets/codex.tsas aPluginTargetDefinition, and along the way corrects three real spec discrepancies — re-verified directly against https://developers.openai.com/codex/plugins/build (fetched twice, independently, for consistency), since Codex has neither a CLI validator nor a vendored schema to check against, unlike claude/cursor.plugin.jsonwas over-validated. Onlynameis actually required; the old validator also requiredversion/description. Fixed — pluginpack still emits both by default (an authoring convenience), it just no longer rejects a manifest that omits them.policy.installation/policy.authentication/categorywere unvalidated. The docs are explicit these are required on every marketplace entry. pluginpack can't infer them, so the base entry stays guess-free (as the existing code comment already intended) andvalidateOutputnow errors clearly when they're missing — instead of silently shipping an incomplete entry. The existing conformance fixture already supplies them via the per-pluginentrypassthrough, so this needed no fixture changes.sourceis only a bare string for local plugins.url/git-subdir/npmsources are structured objects with an innersourcediscriminator (e.g.{ source: "git-subdir", url, path, ref }).validateMarketplaceEntrynow accepts either shape — this is the concrete case that motivated making Codex's validator genuinely custom rather than sharing the string-only validator every other target used.hookswas never declared in the manifest.plugin.jsonnow pointshooksat the bundledhooks/hooks.jsonwhen present, matching howskills/mcpServersare already declared.Updates
CONFORMANCE.md's Codex section, which had pinned a stale, bare-string-only shape from an earlier (2026-06-17) doc retrieval.Stacking
Based on
refactor/claude-target-registry(#15). Merge in order: #14 → #15 → this.Test plan
npm run typechecknpm run lintnpm test(63/63 passing — 4 new Codex regression tests: over-strict manifest validation dropped, missing policy/category caught, structured non-local source accepted, hooks pointer declared)npm run buildnode dist/cli.js docs --check