From d7e055060186649268748cabbc1930f53c703571 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 05:36:05 +0000 Subject: [PATCH 1/7] feat(codex): author marketplace policy and record distribution/install policy parity (#188 S3) Author codex.marketplace displayName, category, and documented policy values (category follows the plugin interface category), admit every documented marketplace source form in the pinned schema, and publish dated four-state distribution rows for marketplace discovery, sources, cache layout, enable state, CLI JSON contracts, feature flags, managed requirements.toml, allow_managed_hooks_only, restrict_to_allowed_sources, and workspace publishing, backed by live codex-cli 0.147.0 probes. --- .changeset/codex-marketplace-policy.md | 5 + packages/agent-bundle/README.md | 12 + .../adapters/capabilities/codex-0.147.0.json | 146 +++++++++++ packages/agent-bundle/src/adapters/codex.ts | 164 +++++++++++- packages/agent-bundle/src/adapters/plugin.ts | 45 +++- .../adapters/schemas/codex/PROVENANCE.json | 12 +- .../schemas/codex/marketplace.schema.json | 70 +++++- .../tests/adapter-metadata.test.ts | 6 +- .../tests/codex-distribution.test.ts | 234 ++++++++++++++++++ .../tests/host-adapters.native.test.ts | 119 +++++++++ 10 files changed, 788 insertions(+), 25 deletions(-) create mode 100644 .changeset/codex-marketplace-policy.md create mode 100644 packages/agent-bundle/tests/codex-distribution.test.ts diff --git a/.changeset/codex-marketplace-policy.md b/.changeset/codex-marketplace-policy.md new file mode 100644 index 000000000..45a7b780d --- /dev/null +++ b/.changeset/codex-marketplace-policy.md @@ -0,0 +1,5 @@ +--- +'agent-bundle': minor +--- + +Author the Codex marketplace entry (`codex.marketplace` displayName, category, and documented `policy.installation` / `policy.authentication` values, with the category following the plugin interface category), admit every documented marketplace source form (local string or object, Git root `url`, `git-subdir`, `npm`) in the pinned marketplace schema, and publish dated four-state Codex distribution rows for marketplace discovery, sources, cache layout, enable state, `codex plugin` / `codex plugin marketplace` JSON contracts, feature flags, managed `requirements.toml`, `allow_managed_hooks_only`, `restrict_to_allowed_sources`, and workspace publishing, backed by live codex-cli 0.147.0 probes. diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index 9eb71ce1f..e727f2920 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -58,6 +58,18 @@ a `matcher` on `UserPromptSubmit` or `Stop`, and a `codex:WebSearch`-style hoste Hook trust (review by current hash in `/hooks`, plugin hooks skipped until trusted, managed hooks immutable) is host-owned and is recorded as unavailable rather than claimed. +The Codex artifact is also its own repo marketplace: `.agents/plugins/marketplace.json` carries one +local `./` entry whose `category` follows the plugin's interface category and whose +`policy.installation` / `policy.authentication` default to `AVAILABLE` / `ON_INSTALL`. +`codex.marketplace` authors the picker `displayName`, the `category`, and the documented policy +values; the pinned marketplace schema also admits Git root (`url`), `git-subdir`, and `npm` sources +for validating real-world marketplaces, but the adapter never emits them. Personal and legacy +`.claude-plugin/marketplace.json` discovery, the `~/.codex/plugins/cache` layout, `config.toml` +enable state, `features.plugins` / `features.hooks`, inline `[hooks]` TOML, `requirements.toml` +managed hooks, `allow_managed_hooks_only`, and `restrict_to_allowed_sources` are host- or +admin-owned and are recorded in `codex-0.147.0.json` (`distribution`) with dated evidence instead of +being claimed. + agent-bundle also owns the npm-facing package build: `bin` entries become self-executing `dist/bin/.js` bundles (shebang, executable bit, generated `main(argv)` envelope) and the optional `lib` entry becomes `dist/.js` with declarations (resolving `typescript` from the diff --git a/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json b/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json index 5527c4730..b4af1eae7 100644 --- a/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json +++ b/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json @@ -12,6 +12,152 @@ "source": "https://developers.openai.com/codex/cli/reference", "state": "supported" }, + "marketplace": { + "policy": { + "authentication": ["ON_INSTALL", "ON_USE"], + "installation": ["AVAILABLE", "INSTALLED_BY_DEFAULT", "NOT_AVAILABLE"] + } + }, + "distribution": { + "repoMarketplaceDiscovery": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins documents repo marketplaces at $REPO_ROOT/.agents/plugins/marketplace.json, resolves each entry's ./-prefixed source.path relative to the marketplace root (not the .agents/plugins folder), and lets codex plugin marketplace add register a local marketplace root directory.", + "retrieved 2026-09-02: the Codex adapter emits .agents/plugins/marketplace.json at the artifact root with one local entry whose source.path is ./, so the artifact is both the marketplace root and the plugin root; live codex-cli 0.147.0 registered such an artifact with codex plugin marketplace add and installed it with codex plugin add @-marketplace in isolated CODEX_HOME probes." + ], + "path": ".agents/plugins/marketplace.json", + "state": "supported" + }, + "personalMarketplaceDiscovery": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins documents the personal marketplace at ~/.agents/plugins/marketplace.json with plugin folders commonly under ./.codex/plugins/; live codex-cli 0.147.0 lists it as the implicit marketplace named personal rooted at $HOME in codex plugin marketplace list --json." + ], + "path": "~/.agents/plugins/marketplace.json", + "reason": "The personal marketplace is a user-owned file in the home directory; the compiler emits artifacts only and the installer registers the artifact root through codex plugin marketplace add instead of editing ~/.agents/plugins/marketplace.json.", + "state": "unavailable" + }, + "legacyClaudeMarketplaceCompatibility": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins lists a legacy-compatible marketplace at $REPO_ROOT/.claude-plugin/marketplace.json alongside the repo and personal marketplace locations." + ], + "path": ".claude-plugin/marketplace.json", + "reason": "The Codex adapter emits the Codex-native .agents/plugins/marketplace.json and never a .claude-plugin/marketplace.json; the legacy path is a host-side reading convenience for Claude-authored repositories, and the unified plugin target's Claude marketplace is emitted by the Claude adapter, not as a Codex compatibility artifact.", + "state": "unavailable" + }, + "marketplaceSources": { + "admitted": ["local", "local-string", "url", "git-subdir", "npm"], + "emitted": "local", + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins documents local entries as { source: \"local\", path: \"./...\" } or a plain ./-prefixed string, Git root entries as source \"url\" with url plus optional ref or sha, Git subdirectory entries as source \"git-subdir\" with url, ./-prefixed path, and ref or sha, and npm entries with required package (scope allowed), optional version (versions, dist-tags, ranges; no path or URL selectors), and optional registry (HTTPS without credentials, query, or fragment).", + "retrieved 2026-09-02: the same page states source.path must stay inside the marketplace root, Codex downloads npm packages without running lifecycle scripts, the npm CLI must be installed with registry authentication from its own configuration, and an unresolvable entry is skipped instead of failing the whole marketplace.", + "retrieved 2026-09-02: the pinned marketplace.schema.json admits every documented source form with root-contained paths, Git URL shapes, hexadecimal sha selectors, and credential-free HTTPS registries; the adapter emits only the local ./ entry that points at the plugin itself." + ], + "reason": "The emitted marketplace always describes the artifact's own plugin root as a local source; Git, git-subdir, and npm sources are validated for real-world marketplaces but not authored, and lifecycle-script prohibition plus unresolved-entry skipping are Codex install-time behavior.", + "state": "degraded" + }, + "marketplaceInterface": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins documents top-level name as the marketplace identifier and interface.displayName as the marketplace title shown in the picker; the adapter emits -marketplace with a displayName that defaults to the plugin name and is authorable through codex.marketplace.displayName." + ], + "state": "supported" + }, + "marketplacePolicy": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins requires policy.installation (AVAILABLE, INSTALLED_BY_DEFAULT, or NOT_AVAILABLE) and policy.authentication (install-time or first-use) on every entry; the adapter defaults to AVAILABLE and ON_INSTALL and accepts codex.marketplace.policy overrides validated against the documented values.", + "retrieved 2026-09-02: live codex-cli 0.147.0 echoed an authored INSTALLED_BY_DEFAULT/ON_USE entry back as installPolicy and authPolicy in codex plugin list --json and authPolicy in codex plugin add --json inside an isolated CODEX_HOME." + ], + "state": "supported" + }, + "marketplaceCategory": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins requires category on every marketplace entry and does not enumerate values; the adapter emits the plugin's interface category (generated Productivity or the authored codex.interface.category) and accepts a codex.marketplace.category override." + ], + "state": "supported" + }, + "installCacheLayout": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins documents installs under ~/.codex/plugins/cache/$MARKETPLACE_NAME/$PLUGIN_NAME/$VERSION/ with $VERSION equal to local for local plugins and the installed copy loaded from that cache path.", + "2026-09-02: live codex-cli 0.147.0 installed a local-marketplace plugin to $CODEX_HOME/plugins/cache//// and reported that path as installedPath; the observed segment was the manifest version (1.0.0), not the documented literal local." + ], + "path": "~/.codex/plugins/cache////", + "reason": "The install cache is created and loaded by the host; the compiler neither writes it nor relies on the documented local version segment, which the pinned 0.147.0 CLI replaces with the manifest version.", + "state": "unavailable" + }, + "pluginEnableState": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins states each plugin's on or off state is stored in ~/.codex/config.toml; live codex-cli 0.147.0 wrote [plugins.\"@\"] enabled = true and a [marketplaces.] table with source_type, source, and last_updated on add, and removed both on codex plugin remove and codex plugin marketplace remove." + ], + "reason": "Enable state lives in the user's config.toml and is toggled by the host (/plugins, codex plugin add and remove); the compiler never writes config.toml.", + "state": "unavailable" + }, + "pluginCliLifecycle": { + "commands": ["add", "list", "remove"], + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/developer-commands?surface=cli documents codex plugin add [--marketplace NAME] [--json], list [--marketplace NAME] [--available --json] [--json], and remove [--json]; add JSON prints pluginId, name, marketplaceName, version, installedPath, and authPolicy; list JSON prints installed and available arrays whose entries include pluginId, name, marketplaceName, version, installed, enabled, source, installPolicy, authPolicy, and optional marketplaceSource; remove JSON prints pluginId, name, and marketplaceName.", + "2026-09-02: live codex-cli 0.147.0 in an isolated CODEX_HOME matched every documented JSON field for add, list, list --available, and remove; agent-bundle install codex runs codex plugin marketplace add followed by codex plugin add @." + ], + "reason": "The installer drives codex plugin add after registering the artifact marketplace, but list and remove remain user-driven and the read-only Doctor pins no Codex inventory verb yet.", + "state": "degraded" + }, + "marketplaceCliLifecycle": { + "commands": ["add", "list", "upgrade", "remove"], + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/developer-commands?surface=cli documents codex plugin marketplace add [--ref REF] [--sparse PATH] [--json] for GitHub shorthand, Git HTTP(S) or SSH URLs, and local roots, list [--json], remove [--json], and upgrade [name] [--json] for Git marketplaces; add JSON prints marketplaceName, installedRoot, and alreadyAdded; list JSON prints marketplaces with name, root, and optional marketplaceSource; upgrade JSON prints selectedMarketplaces, upgradedRoots, and errors; remove JSON prints marketplaceName and installedRoot.", + "2026-09-02: live codex-cli 0.147.0 in an isolated CODEX_HOME matched every documented JSON field; a second add of the same root returned alreadyAdded true, upgrade of a local marketplace exited 1 with `is not configured as a Git marketplace`, and remove of a local marketplace returned installedRoot null." + ], + "reason": "The installer drives codex plugin marketplace add for the artifact root, but list, upgrade, and remove remain user-driven and Git-backed marketplace sources are never emitted.", + "state": "degraded" + }, + "featurePlugins": { + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/enterprise/managed-configuration documents features.plugins = false in requirements.toml to turn plugins off in supported local clients, including API-key sign-in, and https://developers.openai.com/plugins/build/plugins documents features.plugin_sharing = false to disable workspace plugin publishing." + ], + "reason": "features.plugins is user or admin configuration in config.toml or requirements.toml; the compiler emits plugin artifacts and cannot enable plugins where policy disables them.", + "state": "unavailable" + }, + "featureHooks": { + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/hooks documents hooks as enabled by default, turned off with [features] hooks = false, codex_hooks as a deprecated alias, and admins forcing hooks off or pinning them on with [features].hooks in requirements.toml." + ], + "reason": "features.hooks and its deprecated codex_hooks alias are host feature flags in config.toml or requirements.toml; emitted plugin hooks load only where the flag is on.", + "state": "unavailable" + }, + "inlineHooksToml": { + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/hooks documents inline [hooks] tables in config.toml as an alternative to hooks.json per config layer, merged with a startup warning when one layer defines both, and states plugins bundle hooks through the manifest or hooks/hooks.json instead." + ], + "reason": "Inline [hooks] TOML belongs to user, project, and managed config layers; plugins deliver hooks only through hooks/hooks.json or manifest hook entries, which the adapter emits.", + "state": "unavailable" + }, + "managedRequirements": { + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/enterprise/managed-configuration composes requirements.toml from /etc/codex/requirements.toml (or %ProgramData%\\OpenAI\\Codex), cloud-managed bundles, legacy managed_config.toml, and macOS MDM com.openai.codex:requirements_toml_base64, and lets admins define managed hooks under [hooks] with managed_dir and windows_managed_dir plus command_windows overrides while scripts are distributed by MDM.", + "retrieved 2026-09-02: managed hooks are trusted by policy, cannot be disabled from the user hook browser, and must reference absolute script paths under the managed directory." + ], + "reason": "requirements.toml is admin-owned enterprise policy delivered outside the plugin package; the compiler cannot author managed hooks, managed_dir, or MDM payloads.", + "state": "unavailable" + }, + "allowManagedHooksOnly": { + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/hooks and https://learn.chatgpt.com/docs/enterprise/managed-configuration document allow_managed_hooks_only = true as skipping user, project, session, and plugin hooks while still loading requirements.toml and other managed-layer hooks." + ], + "reason": "allow_managed_hooks_only is an admin requirement that disables plugin-bundled hooks outright; emitted hooks cannot opt out of or satisfy it.", + "state": "unavailable" + }, + "restrictToAllowedSources": { + "evidence": [ + "retrieved 2026-09-02: https://learn.chatgpt.com/docs/enterprise/managed-configuration documents [marketplaces] restrict_to_allowed_sources = true with allowed_sources rules of source git (normalized url plus optional exact ref), host_pattern (regex over the lowercase Git host), or local (absolute normalized path); unmatched marketplace add, plugin install, and Git refresh operations are rejected while already configured marketplaces are not filtered at runtime and Codex-managed OpenAI marketplaces stay available." + ], + "reason": "Marketplace source restriction is admin-owned requirements policy applied at add, install, and refresh time; a local artifact marketplace is admitted only if an allowed_sources local rule names its absolute path.", + "state": "unavailable" + }, + "workspacePublishing": { + "evidence": [ + "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins documents workspace admins publishing a personal plugin to selected workspace roles from ChatGPT Plugins, scoped to the workspace boundary and separate from the universal public directory, and features.plugin_sharing = false in cloud-managed requirements to disable it." + ], + "reason": "Workspace publishing is a ChatGPT admin surface with no local artifact or CLI contract; public directory submission is recorded separately as submissionPolicy.", + "state": "unavailable" + } + }, "deferredNativeEvents": { "Interrupt": { "reason": "retrieved 2026-09-02: https://learn.chatgpt.com/docs/hooks now documents Interrupt (main-thread turn interruption; matcher ignored; one-second default and three-second maximum timeout; systemMessage-only output), but the pinned rust-v0.147.0 generated hook schema directory has no interrupt.command.{input,output}.schema.json (those files exist only on main), and Agent Bundle has no canonical interrupt event route (#97 owns route expansion). Deferred until the Codex pin moves to a release that ships the generated Interrupt schemas; authored native Interrupt hooks fail closed with codex.native-hooks.event.deferred.", diff --git a/packages/agent-bundle/src/adapters/codex.ts b/packages/agent-bundle/src/adapters/codex.ts index 396efbb0a..47aa9c0cb 100644 --- a/packages/agent-bundle/src/adapters/codex.ts +++ b/packages/agent-bundle/src/adapters/codex.ts @@ -90,6 +90,21 @@ export interface CodexAuthorConfig { readonly url?: string; } +/** Documented install and authentication policy for the emitted local marketplace entry. */ +export interface CodexMarketplacePolicyConfig { + readonly authentication?: 'ON_INSTALL' | 'ON_USE'; + readonly installation?: 'AVAILABLE' | 'INSTALLED_BY_DEFAULT' | 'NOT_AVAILABLE'; +} + +/** Authored fields of the emitted `.agents/plugins/marketplace.json`; the source stays the local plugin root. */ +export interface CodexMarketplaceConfig { + /** Marketplace-entry category; defaults to the plugin's interface category. */ + readonly category?: string; + /** Marketplace picker title; defaults to the plugin name. */ + readonly displayName?: string; + readonly policy?: CodexMarketplacePolicyConfig; +} + /** Codex-only authored package metadata and install-surface config layered onto the generated manifest. */ export interface CodexHostConfig extends AgentBundleHostConfig { /** Registered MCP connection mappings emitted to the root `.app.json` compatibility document. */ @@ -100,6 +115,7 @@ export interface CodexHostConfig extends AgentBundleHostConfig { readonly interface?: CodexInterfaceConfig; readonly keywords?: readonly string[]; readonly license?: string; + readonly marketplace?: CodexMarketplaceConfig; readonly repository?: string; } @@ -159,7 +175,7 @@ const hookContract = Object.freeze({ wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Codex'), } satisfies TargetHookContract); const metadata = Object.freeze({ - adapterRevision: '1.7.0', + adapterRevision: '1.8.0', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); @@ -180,6 +196,7 @@ const tableCapability = (row: { readonly reason?: string; readonly state: string }; const hookContractTable = capabilityTable.hooks.contract; +const distributionTable = capabilityTable.distribution; const codexReleaseHookEvents: readonly string[] = capabilityTable.hooks.releaseEvents; const codexHookRules = Object.freeze({ additionalContextEvents: hookContractTable.additionalContextLimit.additionalContextEvents as readonly string[], @@ -808,6 +825,114 @@ const planCodexApps = (model: NormalizedPlugin): CodexAppsPlan => { }; }; +const marketplaceTable = capabilityTable.marketplace; +const marketplaceAuthenticationPolicies: readonly string[] = marketplaceTable.policy.authentication; +const marketplaceInstallationPolicies: readonly string[] = marketplaceTable.policy.installation; +const marketplaceConfigFields = Object.freeze(['category', 'displayName', 'policy']); +const marketplacePolicyFields = Object.freeze(['authentication', 'installation']); + +interface CodexMarketplacePlan { + readonly category: string; + readonly diagnostics: readonly Diagnostic[]; + readonly displayName: string; + readonly policy: { readonly authentication: string; readonly installation: string }; + readonly sourceInputs: readonly string[]; +} + +const planCodexMarketplace = (model: NormalizedPlugin, interfaceCategory: string): CodexMarketplacePlan => { + const defaults = { + category: interfaceCategory, + diagnostics: [] as readonly Diagnostic[], + displayName: model.metadata.name, + policy: { authentication: 'ON_INSTALL', installation: 'AVAILABLE' }, + sourceInputs: [] as readonly string[], + }; + const extension = model.extensions[codexName]; + const declared = extension !== undefined && isPlainDataRecord(extension.value) + ? extension.value['marketplace'] + : undefined; + if (declared === undefined || extension === undefined) return defaults; + const inputs = sourceInputs(extension.provenance.sourcePath); + if (!isPlainDataRecord(declared)) { + return { + ...defaults, + diagnostics: [errorDiagnostic( + 'codex.marketplace.invalid', + 'Codex marketplace must be a plain object containing only category, displayName, and policy.', + )], + sourceInputs: inputs, + }; + } + const diagnostics: Diagnostic[] = []; + for (const key of Object.keys(declared)) { + if (!marketplaceConfigFields.includes(key)) { + diagnostics.push(errorDiagnostic( + 'codex.marketplace.field.unknown', + `Codex marketplace field ${JSON.stringify(key)} is not documented; the emitted local marketplace supports category, displayName, and policy.`, + )); + } + } + let category = defaults.category; + let displayName = defaults.displayName; + for (const [field, code] of [['category', 'category'], ['displayName', 'display-name']] as const) { + const authored = declared[field]; + if (authored === undefined) continue; + if (!isNonemptyString(authored)) { + diagnostics.push(errorDiagnostic( + `codex.marketplace.${code}.invalid`, + `Codex marketplace ${field} must be a nonempty string.`, + )); + } else if (field === 'category') { + category = authored; + } else { + displayName = authored; + } + } + const policy = { ...defaults.policy }; + const declaredPolicy = declared['policy']; + if (declaredPolicy !== undefined) { + if (!isPlainDataRecord(declaredPolicy)) { + diagnostics.push(errorDiagnostic( + 'codex.marketplace.policy.invalid', + 'Codex marketplace policy must be a plain object containing installation and authentication.', + )); + } else { + for (const key of Object.keys(declaredPolicy)) { + if (!marketplacePolicyFields.includes(key)) { + diagnostics.push(errorDiagnostic( + 'codex.marketplace.policy.field.unknown', + `Codex marketplace policy field ${JSON.stringify(key)} is not documented.`, + )); + } + } + const authentication = declaredPolicy['authentication']; + if (authentication !== undefined) { + if (typeof authentication !== 'string' || !marketplaceAuthenticationPolicies.includes(authentication)) { + diagnostics.push(errorDiagnostic( + 'codex.marketplace.policy.authentication.invalid', + `Codex marketplace policy.authentication must be one of ${marketplaceAuthenticationPolicies.join(', ')}.`, + )); + } else { + policy.authentication = authentication; + } + } + const installation = declaredPolicy['installation']; + if (installation !== undefined) { + if (typeof installation !== 'string' || !marketplaceInstallationPolicies.includes(installation)) { + diagnostics.push(errorDiagnostic( + 'codex.marketplace.policy.installation.invalid', + `Codex marketplace policy.installation must be one of ${marketplaceInstallationPolicies.join(', ')}.`, + )); + } else { + policy.installation = installation; + } + } + } + } + if (diagnostics.length > 0) return { ...defaults, diagnostics, sourceInputs: inputs }; + return { category, diagnostics, displayName, policy, sourceInputs: inputs }; +}; + const hasLeadingPluginRoot = (value: string): boolean => value === pathTokens.pluginRoot || value.startsWith(`${pathTokens.pluginRoot}/`); @@ -1051,18 +1176,26 @@ export const planCodexArtifacts = ( const pluginValidator = pluginValidatorFor(mcpRelativePath); diagnostics.push(...schemaDiagnostics('plugin', pluginValidator(plugin), pluginValidator.errors)); + const interfaceCategory = interfacePlan.value['category']; + const marketplacePlan = planCodexMarketplace( + model, + typeof interfaceCategory === 'string' ? interfaceCategory : generatedInterface.category, + ); + diagnostics.push(...marketplacePlan.diagnostics); const marketplace = { - interface: { displayName: model.metadata.name }, + interface: { displayName: marketplacePlan.displayName }, name: `${model.metadata.name}-marketplace`, plugins: [{ - category: 'Productivity', + category: marketplacePlan.category, name: model.metadata.name, - policy: { authentication: 'ON_INSTALL', installation: 'AVAILABLE' }, + policy: marketplacePlan.policy, source: { path: './', source: 'local' }, }], }; - const marketplaceValid = validateMarketplace(marketplace); - diagnostics.push(...schemaDiagnostics('marketplace', marketplaceValid, validateMarketplace.errors)); + const marketplaceValid = marketplacePlan.diagnostics.length === 0 && validateMarketplace(marketplace); + if (marketplacePlan.diagnostics.length === 0) { + diagnostics.push(...schemaDiagnostics('marketplace', marketplaceValid, validateMarketplace.errors)); + } const basePlan = standardPluginArtifactPlan({ additionalPluginSourceInputs: sourceInputs( @@ -1085,6 +1218,7 @@ export const planCodexArtifacts = ( isSelected, marketplace, marketplaceRelativePath: codexArtifactPaths.marketplace, + marketplaceSourceInputs: sourceInputs(...marketplacePlan.sourceInputs, ...interfacePlan.sourceInputs), marketplaceValid, mcp, mcpRelativePath, @@ -1158,6 +1292,24 @@ export const codexAdapter: TargetAdapter = Object.freeze({ ), install: supportedCapability(evidence), marketplace: supportedCapability(evidence), + allowManagedHooksOnly: tableCapability(distributionTable.allowManagedHooksOnly), + featureHooks: tableCapability(distributionTable.featureHooks), + featurePlugins: tableCapability(distributionTable.featurePlugins), + inlineHooksToml: tableCapability(distributionTable.inlineHooksToml), + installCacheLayout: tableCapability(distributionTable.installCacheLayout), + legacyClaudeMarketplaceCompatibility: tableCapability(distributionTable.legacyClaudeMarketplaceCompatibility), + managedRequirements: tableCapability(distributionTable.managedRequirements), + marketplaceCategory: tableCapability(distributionTable.marketplaceCategory), + marketplaceCliLifecycle: tableCapability(distributionTable.marketplaceCliLifecycle), + marketplaceInterface: tableCapability(distributionTable.marketplaceInterface), + marketplacePolicy: tableCapability(distributionTable.marketplacePolicy), + marketplaceSources: tableCapability(distributionTable.marketplaceSources), + personalMarketplaceDiscovery: tableCapability(distributionTable.personalMarketplaceDiscovery), + pluginCliLifecycle: tableCapability(distributionTable.pluginCliLifecycle), + pluginEnableState: tableCapability(distributionTable.pluginEnableState), + repoMarketplaceDiscovery: tableCapability(distributionTable.repoMarketplaceDiscovery), + restrictToAllowedSources: tableCapability(distributionTable.restrictToAllowedSources), + workspacePublishing: tableCapability(distributionTable.workspacePublishing), hooks: supportedCapability(evidence), hookAdditionalContextLimit: tableCapability(hookContractTable.additionalContextLimit), hookAsyncCommands: tableCapability(hookContractTable.asyncCommandHooks), diff --git a/packages/agent-bundle/src/adapters/plugin.ts b/packages/agent-bundle/src/adapters/plugin.ts index 064354cde..733e8c25c 100644 --- a/packages/agent-bundle/src/adapters/plugin.ts +++ b/packages/agent-bundle/src/adapters/plugin.ts @@ -112,6 +112,26 @@ const mcpPolicyUnifiedReason = 'The MCP approval policy is enforced by the Codex host at install time; the pinned Claude and Cursor contracts publish no shared per-plugin MCP policy surface.'; const hookContractUnifiedReason = 'The unified bundle emits the Codex-only hook handler contract, but the pinned Claude and Cursor hook contracts declare no shared handler-type, timeout, matcher, or trust surface.'; +const distributionUnifiedReason = + 'The unified bundle emits the Codex-only marketplace and install-policy surface, but the pinned Claude and Cursor contracts declare no shared marketplace source, cache, enable-state, feature-flag, or managed-requirements surface.'; +const codexDistributionCapabilities = [ + 'allowManagedHooksOnly', + 'featureHooks', + 'featurePlugins', + 'inlineHooksToml', + 'installCacheLayout', + 'legacyClaudeMarketplaceCompatibility', + 'managedRequirements', + 'marketplaceCategory', + 'marketplaceInterface', + 'marketplacePolicy', + 'marketplaceSources', + 'personalMarketplaceDiscovery', + 'pluginEnableState', + 'repoMarketplaceDiscovery', + 'restrictToAllowedSources', + 'workspacePublishing', +] as const; const codexHookContractCapabilities = [ 'hookAdditionalContextLimit', 'hookAsyncCommands', @@ -208,7 +228,7 @@ const artifactValidation = deepFreeze({ }); const metadata = Object.freeze({ - adapterRevision: '1.22.0', + adapterRevision: '1.23.0', observedVersion: `${claudeAdapter.metadata.observedVersion}+${codexAdapter.metadata.observedVersion}+${cursorAdapter.metadata.observedVersion}`, // Metadata schemas must exactly match the validation contract: each host's // documents, with one shared Claude-format hook schema (the pinned Codex @@ -601,15 +621,22 @@ const componentCapabilities = Object.freeze(Object.fromEntries( ]), )); -const codexHookContractUnifiedCapabilities = Object.freeze(Object.fromEntries( - codexHookContractCapabilities.map((capability) => [ +const codexHookContractUnifiedCapabilities = Object.freeze(Object.fromEntries([ + ...codexHookContractCapabilities.map((capability) => [ capability, intersectCapabilityStates( codexAdapter.capabilities[capability]!, unavailableCapability(hookContractUnifiedReason), ), ]), -)); + ...codexDistributionCapabilities.map((capability) => [ + capability, + intersectCapabilityStates( + codexAdapter.capabilities[capability]!, + unavailableCapability(distributionUnifiedReason), + ), + ]), +])); const agentCapabilities = Object.freeze(Object.fromEntries( Object.keys(claudeCapabilityTable.plugin.agents).map((rowName) => { @@ -764,7 +791,10 @@ export const pluginAdapter: TargetAdapter = Object.freeze({ ), ), marketplaceCliLifecycle: intersectCapabilityStates( - claudeAdapter.capabilities.marketplaceCliLifecycle!, + intersectCapabilityStates( + claudeAdapter.capabilities.marketplaceCliLifecycle!, + codexAdapter.capabilities.marketplaceCliLifecycle!, + ), unavailableCapability( 'The unified bundle emits host marketplace documents but cannot add, list, remove, or update marketplaces as one cross-host lifecycle transaction.', ), @@ -855,7 +885,10 @@ export const pluginAdapter: TargetAdapter = Object.freeze({ 'The unified bundle emits Claude-only output styles, but the pinned Codex and Cursor contracts declare no shared output styles surface.', ), pluginCliLifecycle: intersectCapabilityStates( - claudeAdapter.capabilities.pluginCliLifecycle!, + intersectCapabilityStates( + claudeAdapter.capabilities.pluginCliLifecycle!, + codexAdapter.capabilities.pluginCliLifecycle!, + ), unavailableCapability( 'The unified bundle emits host artifacts but cannot run Claude-only plugin creation, installation, state, inspection, update, or release commands.', ), diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index 9bd27d70c..d183d09ed 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -13,6 +13,12 @@ "The apps pointer is const-locked to ./.app.json, and app.schema.json requires a nonempty apps map whose entries carry exactly one nonempty registered-connection id.", "Component and interface-asset path patterns treat backslashes as separators and reject them outright so Windows-form parent traversal cannot escape the plugin root; HTTP(S) URL scheme patterns are case-insensitive to match WHATWG URL protocol normalization." ], + "marketplace.schema.json": [ + "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", + "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes rejected, Git URLs must be HTTP(S), ssh://, or scp-style, sha selectors are 7-64 hexadecimal digits, npm versions reject path and URL selectors, and registries must be credential-, query-, and fragment-free HTTPS URLs.", + "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." + ], "hooks.schema.json": [ "The hooks object is closed to the eleven release-documented events; Interrupt is excluded because rust-v0.147.0 ships no generated Interrupt schema.", "Handlers are a closed oneOf of command and mcp_tool shapes; prompt and agent handlers are excluded because Codex parses but skips them.", @@ -31,9 +37,9 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 1671, - "sha256": "1d43c5ed19de401fb7455c5912e4c21113f6e387aef4c28d2eca121f7554c4e8", - "url": "https://github.com/openai/codex/blob/main/codex-rs/skills/src/assets/samples/plugin-creator/references/plugin-json-spec.md" + "bytes": 3671, + "sha256": "0c8f05730a602ed4d7dfd4b8bb8091b251deb3f002bf6b0c89e9b64eac349f61", + "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { "bytes": 1267, diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index 61f2d8f67..63457c494 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -1,6 +1,62 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://agent-bundle.dev/schemas/codex/0.147.0/marketplace.schema.json", + "$defs": { + "containedPath": { + "pattern": "^\\./(?!(?:.*[\\/\\\\])?\\.\\.(?:[\\/\\\\]|$))[^\\\\]*$", + "type": "string" + }, + "gitUrl": { + "minLength": 1, + "pattern": "^(?:[Hh][Tt][Tt][Pp][Ss]?://|[Ss][Ss][Hh]://|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:)\\S+$", + "type": "string" + }, + "gitRef": { "minLength": 1, "type": "string" }, + "gitSha": { "minLength": 1, "pattern": "^[0-9A-Fa-f]{7,64}$", "type": "string" }, + "localSource": { + "additionalProperties": false, + "properties": { + "path": { "$ref": "#/$defs/containedPath" }, + "source": { "const": "local", "type": "string" } + }, + "required": ["source", "path"], + "type": "object" + }, + "gitRootSource": { + "additionalProperties": false, + "properties": { + "ref": { "$ref": "#/$defs/gitRef" }, + "sha": { "$ref": "#/$defs/gitSha" }, + "source": { "const": "url", "type": "string" }, + "url": { "$ref": "#/$defs/gitUrl" } + }, + "required": ["source", "url"], + "type": "object" + }, + "gitSubdirSource": { + "additionalProperties": false, + "properties": { + "path": { "$ref": "#/$defs/containedPath" }, + "ref": { "$ref": "#/$defs/gitRef" }, + "sha": { "$ref": "#/$defs/gitSha" }, + "source": { "const": "git-subdir", "type": "string" }, + "url": { "$ref": "#/$defs/gitUrl" } + }, + "required": ["source", "url", "path"], + "type": "object" + }, + "npmSource": { + "additionalProperties": false, + "properties": { + "package": { "minLength": 1, "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$", "type": "string" }, + "registry": { "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[^@?#\\s]+$", "type": "string" }, + "source": { "const": "npm", "type": "string" }, + "version": { "minLength": 1, "pattern": "^[^/\\\\:@]+$", "type": "string" } + }, + "required": ["source", "package"], + "type": "object" + } + }, "additionalProperties": false, "properties": { "interface": { @@ -26,13 +82,13 @@ "type": "object" }, "source": { - "additionalProperties": false, - "properties": { - "path": { "pattern": "^\\./", "type": "string" }, - "source": { "const": "local", "type": "string" } - }, - "required": ["source", "path"], - "type": "object" + "oneOf": [ + { "$ref": "#/$defs/containedPath" }, + { "$ref": "#/$defs/localSource" }, + { "$ref": "#/$defs/gitRootSource" }, + { "$ref": "#/$defs/gitSubdirSource" }, + { "$ref": "#/$defs/npmSource" } + ] } }, "required": ["name", "source", "policy", "category"], diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index d205236af..2eb3eb897 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -68,7 +68,7 @@ it('records exact immutable metadata for every built-in target', () => { ], }); expect(registryMetadata(registry, 'codex')).toEqual({ - adapterRevision: '1.7.0', + adapterRevision: '1.8.0', observedVersion: '0.147.0', schemas: [ { @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: '1d43c5ed19de401fb7455c5912e4c21113f6e387aef4c28d2eca121f7554c4e8', + sha256: '0c8f05730a602ed4d7dfd4b8bb8091b251deb3f002bf6b0c89e9b64eac349f61', }, { name: 'mcp', @@ -170,7 +170,7 @@ it('records exact immutable metadata for every built-in target', () => { }, ], }); - expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.22.0'); + expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.23.0'); }); it('records observed capability versions and rehashes schema snapshots against pinned provenance', async () => { diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts new file mode 100644 index 000000000..f6a180e05 --- /dev/null +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -0,0 +1,234 @@ +import { expect, it } from '@rstest/core'; + +import codexCapabilityTable from '../src/adapters/capabilities/codex-0.147.0.json' with { type: 'json' }; +import { codexAdapter } from '../src/adapters/codex.ts'; +import { createDefaultRegistry } from '../src/adapters/registry.ts'; +import marketplaceSchema from '../src/adapters/schemas/codex/marketplace.schema.json' with { type: 'json' }; +import { createAdapterValidator } from '../src/adapters/types.ts'; +import type { NormalizedPlugin } from '../src/core/types.ts'; + +const distributionRows = { + allowManagedHooksOnly: 'unavailable', + featureHooks: 'unavailable', + featurePlugins: 'unavailable', + inlineHooksToml: 'unavailable', + installCacheLayout: 'unavailable', + legacyClaudeMarketplaceCompatibility: 'unavailable', + managedRequirements: 'unavailable', + marketplaceCategory: 'supported', + marketplaceCliLifecycle: 'degraded', + marketplaceInterface: 'supported', + marketplacePolicy: 'supported', + marketplaceSources: 'degraded', + personalMarketplaceDiscovery: 'unavailable', + pluginCliLifecycle: 'degraded', + pluginEnableState: 'unavailable', + repoMarketplaceDiscovery: 'supported', + restrictToAllowedSources: 'unavailable', + workspacePublishing: 'unavailable', +} as const; + +const plugin: NormalizedPlugin = Object.freeze({ + extensions: Object.freeze({}), + hooks: Object.freeze([]), + marketplace: true as const, + mcpServers: Object.freeze([]), + metadata: Object.freeze({ + description: 'Review code and explain findings.', + id: 'plugin:review-tools', + name: 'review-tools', + provenance: Object.freeze({ kind: 'config' as const, sourcePath: '/workspace/agent-bundle.config.ts' }), + version: '1.2.3', + }), + runtime: Object.freeze({ node: '22.12.0' }), + scripts: Object.freeze([]), + skills: Object.freeze([]), + targets: Object.freeze([ + Object.freeze({ id: 'target:codex', name: 'codex', provenance: Object.freeze({ kind: 'config' as const, sourcePath: '/workspace/agent-bundle.config.ts' }) }), + ]), +}); + +const withCodexConfig = (value: Readonly>): NormalizedPlugin => ({ + ...plugin, + extensions: { + codex: { + id: 'extension:codex', + key: 'codex', + provenance: { kind: 'config', sourcePath: '/workspace/codex.config.ts' }, + target: 'codex', + value, + }, + }, +}); + +const emittedMarketplace = (model: NormalizedPlugin) => { + const plan = codexAdapter.plan(model); + const entry = plan.entries.find((candidate) => candidate.relativePath === '.agents/plugins/marketplace.json'); + return { + codes: plan.diagnostics.map((diagnostic) => diagnostic.code), + document: entry?.kind === 'write' ? JSON.parse(entry.content) as Record : undefined, + entry, + }; +}; + +const entry = (source: unknown) => ({ + category: 'Productivity', + name: 'remote-helper', + policy: { authentication: 'ON_INSTALL', installation: 'AVAILABLE' }, + source, +}); + +const marketplace = (...sources: readonly unknown[]) => ({ + interface: { displayName: 'Local Example Plugins' }, + name: 'local-example-plugins', + plugins: sources.map(entry), +}); + +it('records dated four-state Codex distribution rows mirrored by the adapter and intersected by the unified bundle', () => { + const registry = createDefaultRegistry(); + const unified = registry.get('plugin'); + const table = codexCapabilityTable.distribution as Readonly>; + + expect(Object.keys(table).sort()).toEqual(Object.keys(distributionRows).sort()); + for (const [capability, expectedState] of Object.entries(distributionRows)) { + const row = table[capability]!; + expect(row.state, capability).toBe(expectedState); + expect(row.evidence.length, capability).toBeGreaterThan(0); + expect(row.evidence.every((line) => /^(?:retrieved )?2026-09-02:/u.test(line)), capability).toBe(true); + if (expectedState === 'supported') { + expect(row.reason).toBeUndefined(); + expect(codexAdapter.capabilities[capability]).toEqual({ + evidence: { observedVersion: '0.147.0', target: 'codex' }, + state: 'supported', + }); + } else { + expect(row.reason, capability).toMatch(/\S/u); + expect(codexAdapter.capabilities[capability]).toMatchObject({ reason: row.reason, state: expectedState }); + } + expect(registry.supports('codex', capability)).toBe(expectedState === 'supported'); + expect(unified.capabilities[capability]).toMatchObject({ state: 'unavailable' }); + expect(registry.supports('plugin', capability)).toBe(false); + } + expect(table.pluginCliLifecycle).toMatchObject({ commands: ['add', 'list', 'remove'] }); + expect(table.marketplaceCliLifecycle).toMatchObject({ commands: ['add', 'list', 'upgrade', 'remove'] }); + expect(table.marketplaceSources).toMatchObject({ + admitted: ['local', 'local-string', 'url', 'git-subdir', 'npm'], + emitted: 'local', + }); + expect(table.installCacheLayout).toMatchObject({ + evidence: expect.arrayContaining([expect.stringContaining('manifest version')]), + }); + expect(codexCapabilityTable.marketplace.policy).toEqual({ + authentication: ['ON_INSTALL', 'ON_USE'], + installation: ['AVAILABLE', 'INSTALLED_BY_DEFAULT', 'NOT_AVAILABLE'], + }); +}); + +it('admits every documented marketplace source form and rejects escapes, credentials, and selectors', () => { + const validate = createAdapterValidator().compile(marketplaceSchema); + const admitted = marketplace( + './plugins/my-plugin', + { path: './plugins/my-plugin', source: 'local' }, + { ref: 'main', source: 'url', url: 'https://github.com/example/codex-plugins.git' }, + { sha: '0123456789abcdef0123456789abcdef01234567', source: 'url', url: 'git@github.com:example/codex-plugins.git' }, + { path: './plugins/remote-helper', ref: 'main', source: 'git-subdir', url: 'https://github.com/example/codex-plugins.git' }, + { path: './plugins/remote-helper', source: 'git-subdir', url: 'ssh://git@github.com/example/codex-plugins.git' }, + { package: '@example/codex-plugin', registry: 'https://registry.npmjs.org', source: 'npm', version: '^1.2.0' }, + { package: 'codex-plugin', source: 'npm' }, + { package: 'codex-plugin', source: 'npm', version: 'latest' }, + ); + expect(validate(admitted), JSON.stringify(validate.errors)).toBe(true); + + const rejected: readonly unknown[] = [ + '../outside', + 'plugins/my-plugin', + { path: './plugins/../../outside', source: 'local' }, + { path: '.\\plugins\\my-plugin', source: 'local' }, + { source: 'url', url: 'file:///tmp/plugins' }, + { source: 'url', url: 'https://github.com/example/codex-plugins.git', sha: 'not-hex' }, + { source: 'git-subdir', url: 'https://github.com/example/codex-plugins.git' }, + { package: '@example/codex-plugin', source: 'npm', version: 'file:../local' }, + { package: '@example/codex-plugin', source: 'npm', version: 'https://example.test/pkg.tgz' }, + { package: '@example/codex-plugin', source: 'npm', version: 'npm:other@1.0.0' }, + { package: '@example/codex-plugin', registry: 'http://registry.example.test', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://user:token@registry.example.test', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://registry.example.test/?auth=1', source: 'npm' }, + { source: 'npm' }, + { source: 'git', url: 'https://github.com/example/codex-plugins.git' }, + ]; + for (const source of rejected) { + expect(validate(marketplace(source)), JSON.stringify(source)).toBe(false); + } + for (const policy of [ + { authentication: 'ON_INSTALL' }, + { authentication: 'ALWAYS', installation: 'AVAILABLE' }, + { authentication: 'ON_INSTALL', installation: 'HIDDEN' }, + ]) { + expect(validate({ ...marketplace('./'), plugins: [{ ...entry('./'), policy }] }), JSON.stringify(policy)).toBe(false); + } + expect(validate({ ...marketplace('./'), plugins: [{ ...entry('./'), category: undefined }] })).toBe(false); +}); + +it('emits the marketplace entry with documented defaults and follows the authored interface category', () => { + const generated = emittedMarketplace(plugin); + expect(generated.codes).toEqual([]); + expect(generated.document).toEqual({ + interface: { displayName: 'review-tools' }, + name: 'review-tools-marketplace', + plugins: [{ + category: 'Productivity', + name: 'review-tools', + policy: { authentication: 'ON_INSTALL', installation: 'AVAILABLE' }, + source: { path: './', source: 'local' }, + }], + }); + + const categorized = emittedMarketplace(withCodexConfig({ interface: { category: 'Developer Tools' } })); + expect(categorized.codes).toEqual([]); + expect(categorized.document).toMatchObject({ plugins: [{ category: 'Developer Tools' }] }); + expect(categorized.entry).toMatchObject({ sourceInputs: expect.arrayContaining(['/workspace/codex.config.ts']) }); +}); + +it('emits every authored Codex marketplace field', () => { + const authored = emittedMarketplace(withCodexConfig({ + interface: { category: 'Developer Tools' }, + marketplace: { + category: 'Security', + displayName: 'Review Tools Marketplace', + policy: { authentication: 'ON_USE', installation: 'INSTALLED_BY_DEFAULT' }, + }, + })); + expect(authored.codes).toEqual([]); + expect(authored.document).toEqual({ + interface: { displayName: 'Review Tools Marketplace' }, + name: 'review-tools-marketplace', + plugins: [{ + category: 'Security', + name: 'review-tools', + policy: { authentication: 'ON_USE', installation: 'INSTALLED_BY_DEFAULT' }, + source: { path: './', source: 'local' }, + }], + }); + expect(authored.entry).toMatchObject({ sourceInputs: expect.arrayContaining(['/workspace/codex.config.ts']) }); +}); + +it.each([ + { code: 'codex.marketplace.invalid', value: [] }, + { code: 'codex.marketplace.invalid', value: 'AVAILABLE' }, + { code: 'codex.marketplace.field.unknown', value: { source: { source: 'npm', package: 'x' } } }, + { code: 'codex.marketplace.category.invalid', value: { category: '' } }, + { code: 'codex.marketplace.display-name.invalid', value: { displayName: ' ' } }, + { code: 'codex.marketplace.policy.invalid', value: { policy: 'ON_USE' } }, + { code: 'codex.marketplace.policy.field.unknown', value: { policy: { approval: 'prompt' } } }, + { code: 'codex.marketplace.policy.authentication.invalid', value: { policy: { authentication: 'ALWAYS' } } }, + { code: 'codex.marketplace.policy.installation.invalid', value: { policy: { installation: 'HIDDEN' } } }, +] as const)('rejects invalid authored Codex marketplace input with $code', ({ code, value }) => { + const rejected = emittedMarketplace(withCodexConfig({ marketplace: value })); + + expect(rejected.codes).toContain(code); + expect(rejected.document).toBeUndefined(); +}); diff --git a/packages/agent-bundle/tests/host-adapters.native.test.ts b/packages/agent-bundle/tests/host-adapters.native.test.ts index 0e8afcbd8..309a3a5bc 100644 --- a/packages/agent-bundle/tests/host-adapters.native.test.ts +++ b/packages/agent-bundle/tests/host-adapters.native.test.ts @@ -368,6 +368,125 @@ nativeIt('installs and lists an emitted Codex plugin carrying the complete inter } }); +nativeIt('pins the Codex plugin and marketplace CLI JSON contracts, cache layout, and enable state for an authored marketplace policy', async () => { + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-codex-marketplace-policy-')); + const pluginRoot = join(root, 'plugin'); + const codexHome = join(root, 'codex-home'); + const policyModel: NormalizedPlugin = { + ...model, + extensions: { + codex: { + id: 'extension:codex', + key: 'codex', + provenance: { kind: 'config', sourcePath: '/workspace/codex.config.ts' }, + target: 'codex', + value: { + interface: { category: 'Developer Tools' }, + marketplace: { + displayName: 'Review Tools Marketplace', + policy: { authentication: 'ON_USE', installation: 'INSTALLED_BY_DEFAULT' }, + }, + }, + }, + }, + }; + const pluginId = 'review-tools@review-tools-marketplace'; + + try { + await Promise.all([ + mkdir(pluginRoot, { recursive: true }), + mkdir(codexHome, { recursive: true }), + ]); + const plan = codexAdapter.plan(policyModel); + expect(plan.diagnostics).toEqual([]); + await emitPlanEntries({ entries: plan.entries, root: pluginRoot }); + + const added = await runCodex(root, ['plugin', 'marketplace', 'add', pluginRoot, '--json'], codexHome); + expect(added.code, added.stderr).toBe(0); + expect(JSON.parse(added.stdout)).toEqual({ + alreadyAdded: false, + installedRoot: pluginRoot, + marketplaceName: 'review-tools-marketplace', + }); + const readded = await runCodex(root, ['plugin', 'marketplace', 'add', pluginRoot, '--json'], codexHome); + expect(readded.code, readded.stderr).toBe(0); + expect(JSON.parse(readded.stdout)).toMatchObject({ alreadyAdded: true }); + + const marketplaces = await runCodex(root, ['plugin', 'marketplace', 'list', '--json'], codexHome); + expect(marketplaces.code, marketplaces.stderr).toBe(0); + expect(JSON.parse(marketplaces.stdout)).toMatchObject({ + marketplaces: expect.arrayContaining([{ + marketplaceSource: { source: pluginRoot, sourceType: 'local' }, + name: 'review-tools-marketplace', + root: pluginRoot, + }]), + }); + + const installed = await runCodex(root, ['plugin', 'add', pluginId, '--json'], codexHome); + expect(installed.code, installed.stderr).toBe(0); + const installedDocument = JSON.parse(installed.stdout) as { readonly installedPath: string }; + expect(installedDocument).toEqual({ + authPolicy: 'ON_USE', + installedPath: join(codexHome, 'plugins', 'cache', 'review-tools-marketplace', 'review-tools', '1.2.3'), + marketplaceName: 'review-tools-marketplace', + name: 'review-tools', + pluginId, + version: '1.2.3', + }); + // The documented cache layout names a literal `local` version segment for + // local plugins; the pinned 0.147.0 CLI uses the manifest version instead. + await access(join(installedDocument.installedPath, '.codex-plugin', 'plugin.json')); + const enableState = await readFile(join(codexHome, 'config.toml'), 'utf8'); + expect(enableState).toContain(`[plugins."${pluginId}"]\nenabled = true`); + expect(enableState).toContain('[marketplaces.review-tools-marketplace]'); + expect(enableState).toContain('source_type = "local"'); + + const listed = await runCodex(root, ['plugin', 'list', '--json'], codexHome); + expect(listed.code, listed.stderr).toBe(0); + expect(JSON.parse(listed.stdout)).toEqual({ + available: [], + installed: [{ + authPolicy: 'ON_USE', + enabled: true, + installPolicy: 'INSTALLED_BY_DEFAULT', + installed: true, + marketplaceName: 'review-tools-marketplace', + marketplaceSource: { source: pluginRoot, sourceType: 'local' }, + name: 'review-tools', + pluginId, + source: { path: pluginRoot, source: 'local' }, + version: '1.2.3', + }], + }); + + const upgraded = await runCodex(root, ['plugin', 'marketplace', 'upgrade', '--json'], codexHome); + expect(upgraded.code, upgraded.stderr).toBe(0); + expect(JSON.parse(upgraded.stdout)).toEqual({ errors: [], selectedMarketplaces: [], upgradedRoots: [] }); + const upgradeLocal = await runCodex(root, ['plugin', 'marketplace', 'upgrade', 'review-tools-marketplace', '--json'], codexHome); + expect(upgradeLocal.code).toBe(1); + expect(upgradeLocal.stderr).toContain('is not configured as a Git marketplace'); + + const removed = await runCodex(root, ['plugin', 'remove', pluginId, '--json'], codexHome); + expect(removed.code, removed.stderr).toBe(0); + expect(JSON.parse(removed.stdout)).toEqual({ + marketplaceName: 'review-tools-marketplace', + name: 'review-tools', + pluginId, + }); + const marketplaceRemoved = await runCodex(root, ['plugin', 'marketplace', 'remove', 'review-tools-marketplace', '--json'], codexHome); + expect(marketplaceRemoved.code, marketplaceRemoved.stderr).toBe(0); + expect(JSON.parse(marketplaceRemoved.stdout)).toEqual({ + installedRoot: null, + marketplaceName: 'review-tools-marketplace', + }); + const cleared = await readFile(join(codexHome, 'config.toml'), 'utf8'); + expect(cleared).not.toContain(pluginId); + expect(cleared).not.toContain('[marketplaces.review-tools-marketplace]'); + } finally { + await rm(root, { force: true, recursive: true }); + } +}); + nativeIt('pins Claude plugin and marketplace lifecycle command help', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-lifecycle-help-')); From a809d0d274bb317f6b6a9ee46f19093179097342 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:00:15 +0000 Subject: [PATCH 2/7] fix(codex): refuse NOT_AVAILABLE for the self-installing marketplace and validate Git/registry URL structure Live codex-cli 0.147.0 registers a NOT_AVAILABLE marketplace entry but refuses `codex plugin add`, the exact command INSTALL.md and installBundle() run, so an authored NOT_AVAILABLE now fails the build with codex.marketplace.policy.installation.not-installable instead of emitting an uninstallable bundle. The pinned marketplace schema's Git URL and npm registry patterns now require a syntactically valid hostname, optional port, and a percent-encoded path rather than a bare scheme prefix. Addresses the two Codex review threads on #392. --- .changeset/codex-marketplace-policy.md | 2 +- packages/agent-bundle/README.md | 8 ++- .../adapters/capabilities/codex-0.147.0.json | 7 ++- packages/agent-bundle/src/adapters/codex.ts | 11 ++++ .../adapters/schemas/codex/PROVENANCE.json | 6 +-- .../schemas/codex/marketplace.schema.json | 7 ++- .../tests/adapter-metadata.test.ts | 2 +- .../tests/codex-distribution.test.ts | 52 ++++++++++++++++++- 8 files changed, 83 insertions(+), 12 deletions(-) diff --git a/.changeset/codex-marketplace-policy.md b/.changeset/codex-marketplace-policy.md index 45a7b780d..714004378 100644 --- a/.changeset/codex-marketplace-policy.md +++ b/.changeset/codex-marketplace-policy.md @@ -2,4 +2,4 @@ 'agent-bundle': minor --- -Author the Codex marketplace entry (`codex.marketplace` displayName, category, and documented `policy.installation` / `policy.authentication` values, with the category following the plugin interface category), admit every documented marketplace source form (local string or object, Git root `url`, `git-subdir`, `npm`) in the pinned marketplace schema, and publish dated four-state Codex distribution rows for marketplace discovery, sources, cache layout, enable state, `codex plugin` / `codex plugin marketplace` JSON contracts, feature flags, managed `requirements.toml`, `allow_managed_hooks_only`, `restrict_to_allowed_sources`, and workspace publishing, backed by live codex-cli 0.147.0 probes. +Author the Codex marketplace entry (`codex.marketplace` displayName, category, and documented `policy.installation` / `policy.authentication` values, with the category following the plugin interface category; `installation: NOT_AVAILABLE` fails the build with `codex.marketplace.policy.installation.not-installable` because live `codex plugin add` refuses it and the emitted `INSTALL.md` / `installBundle()` run that command), admit every documented marketplace source form (local string or object, Git root `url`, `git-subdir`, `npm`) in the pinned marketplace schema with structurally validated Git and registry URLs, and publish dated four-state Codex distribution rows for marketplace discovery, sources, cache layout, enable state, `codex plugin` / `codex plugin marketplace` JSON contracts, feature flags, managed `requirements.toml`, `allow_managed_hooks_only`, `restrict_to_allowed_sources`, and workspace publishing, backed by live codex-cli 0.147.0 probes. diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index e727f2920..1b1631253 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -62,8 +62,12 @@ The Codex artifact is also its own repo marketplace: `.agents/plugins/marketplac local `./` entry whose `category` follows the plugin's interface category and whose `policy.installation` / `policy.authentication` default to `AVAILABLE` / `ON_INSTALL`. `codex.marketplace` authors the picker `displayName`, the `category`, and the documented policy -values; the pinned marketplace schema also admits Git root (`url`), `git-subdir`, and `npm` sources -for validating real-world marketplaces, but the adapter never emits them. Personal and legacy +values, except `installation: NOT_AVAILABLE`, which fails the build +(`codex.marketplace.policy.installation.not-installable`) because live `codex plugin add` refuses +such entries and that is exactly the command the emitted `INSTALL.md` and `installBundle()` run. +The pinned marketplace schema also admits Git root (`url`), `git-subdir`, and `npm` sources for +validating real-world marketplaces (Git and registry URLs must carry a syntactically valid +hostname and path, not just a scheme prefix), but the adapter never emits them. Personal and legacy `.claude-plugin/marketplace.json` discovery, the `~/.codex/plugins/cache` layout, `config.toml` enable state, `features.plugins` / `features.hooks`, inline `[hooks]` TOML, `requirements.toml` managed hooks, `allow_managed_hooks_only`, and `restrict_to_allowed_sources` are host- or diff --git a/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json b/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json index b4af1eae7..d1217d565 100644 --- a/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json +++ b/packages/agent-bundle/src/adapters/capabilities/codex-0.147.0.json @@ -15,7 +15,8 @@ "marketplace": { "policy": { "authentication": ["ON_INSTALL", "ON_USE"], - "installation": ["AVAILABLE", "INSTALLED_BY_DEFAULT", "NOT_AVAILABLE"] + "installation": ["AVAILABLE", "INSTALLED_BY_DEFAULT", "NOT_AVAILABLE"], + "notInstallable": "NOT_AVAILABLE" } }, "distribution": { @@ -63,8 +64,10 @@ "marketplacePolicy": { "evidence": [ "retrieved 2026-09-02: https://developers.openai.com/plugins/build/plugins requires policy.installation (AVAILABLE, INSTALLED_BY_DEFAULT, or NOT_AVAILABLE) and policy.authentication (install-time or first-use) on every entry; the adapter defaults to AVAILABLE and ON_INSTALL and accepts codex.marketplace.policy overrides validated against the documented values.", - "retrieved 2026-09-02: live codex-cli 0.147.0 echoed an authored INSTALLED_BY_DEFAULT/ON_USE entry back as installPolicy and authPolicy in codex plugin list --json and authPolicy in codex plugin add --json inside an isolated CODEX_HOME." + "retrieved 2026-09-02: live codex-cli 0.147.0 echoed an authored INSTALLED_BY_DEFAULT/ON_USE entry back as installPolicy and authPolicy in codex plugin list --json and authPolicy in codex plugin add --json inside an isolated CODEX_HOME.", + "retrieved 2026-09-03: live codex-cli 0.147.0 accepted codex plugin marketplace add for a NOT_AVAILABLE entry but refused codex plugin add with `plugin is not available for install in marketplace `; because the emitted INSTALL.md and installBundle() run exactly that command, the adapter rejects authored NOT_AVAILABLE with codex.marketplace.policy.installation.not-installable instead of emitting a bundle whose own install path fails." ], + "rejected": ["NOT_AVAILABLE"], "state": "supported" }, "marketplaceCategory": { diff --git a/packages/agent-bundle/src/adapters/codex.ts b/packages/agent-bundle/src/adapters/codex.ts index 47aa9c0cb..cb558df8b 100644 --- a/packages/agent-bundle/src/adapters/codex.ts +++ b/packages/agent-bundle/src/adapters/codex.ts @@ -828,6 +828,7 @@ const planCodexApps = (model: NormalizedPlugin): CodexAppsPlan => { const marketplaceTable = capabilityTable.marketplace; const marketplaceAuthenticationPolicies: readonly string[] = marketplaceTable.policy.authentication; const marketplaceInstallationPolicies: readonly string[] = marketplaceTable.policy.installation; +const marketplaceNotInstallablePolicy: string = marketplaceTable.policy.notInstallable; const marketplaceConfigFields = Object.freeze(['category', 'displayName', 'policy']); const marketplacePolicyFields = Object.freeze(['authentication', 'installation']); @@ -923,6 +924,16 @@ const planCodexMarketplace = (model: NormalizedPlugin, interfaceCategory: string 'codex.marketplace.policy.installation.invalid', `Codex marketplace policy.installation must be one of ${marketplaceInstallationPolicies.join(', ')}.`, )); + } else if (installation === marketplaceNotInstallablePolicy) { + // The emitted marketplace exists to install this artifact: INSTALL.md and + // installBundle() both run `codex plugin add`, which the host refuses for + // NOT_AVAILABLE entries, so a self-installing bundle cannot honestly carry it. + diagnostics.push(errorDiagnostic( + 'codex.marketplace.policy.installation.not-installable', + `Codex marketplace policy.installation ${marketplaceNotInstallablePolicy} makes the emitted bundle refuse its own codex plugin add install path; use ${ + marketplaceInstallationPolicies.filter((value) => value !== marketplaceNotInstallablePolicy).join(' or ') + }.`, + )); } else { policy.installation = installation; } diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index d183d09ed..e158baed4 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -16,7 +16,7 @@ "marketplace.schema.json": [ "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", - "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes rejected, Git URLs must be HTTP(S), ssh://, or scp-style, sha selectors are 7-64 hexadecimal digits, npm versions reject path and URL selectors, and registries must be credential-, query-, and fragment-free HTTPS URLs.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid hostname (optional userinfo and port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions reject path and URL selectors, and registries must be credential-, query-, and fragment-free HTTPS URLs with the same hostname grammar.", "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." ], "hooks.schema.json": [ @@ -37,8 +37,8 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 3671, - "sha256": "0c8f05730a602ed4d7dfd4b8bb8091b251deb3f002bf6b0c89e9b64eac349f61", + "bytes": 4213, + "sha256": "a4bca8063782e1bef29734f570c7e93b9c097f0a4dc676b6990f0a0122fd3b30", "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index 63457c494..3d10a394b 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -8,7 +8,7 @@ }, "gitUrl": { "minLength": 1, - "pattern": "^(?:[Hh][Tt][Tt][Pp][Ss]?://|[Ss][Ss][Hh]://|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:)\\S+$", + "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::[0-9]{1,5})?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*:(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", "type": "string" }, "gitRef": { "minLength": 1, "type": "string" }, @@ -49,7 +49,10 @@ "additionalProperties": false, "properties": { "package": { "minLength": 1, "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$", "type": "string" }, - "registry": { "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[^@?#\\s]+$", "type": "string" }, + "registry": { + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::[0-9]{1,5})?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", + "type": "string" + }, "source": { "const": "npm", "type": "string" }, "version": { "minLength": 1, "pattern": "^[^/\\\\:@]+$", "type": "string" } }, diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index 2eb3eb897..6836a7638 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: '0c8f05730a602ed4d7dfd4b8bb8091b251deb3f002bf6b0c89e9b64eac349f61', + sha256: 'a4bca8063782e1bef29734f570c7e93b9c097f0a4dc676b6990f0a0122fd3b30', }, { name: 'mcp', diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts index f6a180e05..9b6930b48 100644 --- a/packages/agent-bundle/tests/codex-distribution.test.ts +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -98,7 +98,7 @@ it('records dated four-state Codex distribution rows mirrored by the adapter and const row = table[capability]!; expect(row.state, capability).toBe(expectedState); expect(row.evidence.length, capability).toBeGreaterThan(0); - expect(row.evidence.every((line) => /^(?:retrieved )?2026-09-02:/u.test(line)), capability).toBe(true); + expect(row.evidence.every((line) => /^(?:retrieved )?2026-09-0[23]:/u.test(line)), capability).toBe(true); if (expectedState === 'supported') { expect(row.reason).toBeUndefined(); expect(codexAdapter.capabilities[capability]).toEqual({ @@ -125,6 +125,7 @@ it('records dated four-state Codex distribution rows mirrored by the adapter and expect(codexCapabilityTable.marketplace.policy).toEqual({ authentication: ['ON_INSTALL', 'ON_USE'], installation: ['AVAILABLE', 'INSTALLED_BY_DEFAULT', 'NOT_AVAILABLE'], + notInstallable: 'NOT_AVAILABLE', }); }); @@ -140,6 +141,11 @@ it('admits every documented marketplace source form and rejects escapes, credent { package: '@example/codex-plugin', registry: 'https://registry.npmjs.org', source: 'npm', version: '^1.2.0' }, { package: 'codex-plugin', source: 'npm' }, { package: 'codex-plugin', source: 'npm', version: 'latest' }, + { source: 'url', url: 'https://token@git.example.test:8443/team/codex-plugins.git' }, + { source: 'url', url: 'ssh://git@git.example.test:2222/team/codex-plugins.git' }, + { source: 'url', url: 'git@git.example.test:~team/codex-plugins.git' }, + { source: 'url', url: 'https://github.com/example/codex%20plugins.git' }, + { package: 'codex-plugin', registry: 'https://npm.example.test:4873/prefix/', source: 'npm' }, ); expect(validate(admitted), JSON.stringify(validate.errors)).toBe(true); @@ -149,8 +155,27 @@ it('admits every documented marketplace source form and rejects escapes, credent { path: './plugins/../../outside', source: 'local' }, { path: '.\\plugins\\my-plugin', source: 'local' }, { source: 'url', url: 'file:///tmp/plugins' }, + // Scheme-prefix matches with an unusable authority or path are not URLs. + { source: 'url', url: 'https://%zz/repository.git' }, + { source: 'url', url: 'https://' }, + { source: 'url', url: 'https://github.com' }, + { source: 'url', url: 'https://github.com/example/codex%2plugins.git' }, + { source: 'url', url: 'https://git hub.com/example/codex-plugins.git' }, + { source: 'url', url: 'https://-github.com/example/codex-plugins.git' }, + { source: 'url', url: 'https://github.com:port/example/codex-plugins.git' }, + { source: 'url', url: 'https://github.com/example/codex-plugins.git?ref=main' }, + { source: 'url', url: 'ssh://%zz/repository.git' }, + { source: 'url', url: 'ssh://github.com' }, + { source: 'url', url: 'git@%zz:example/codex-plugins.git' }, + { source: 'url', url: 'git@github.com:' }, + { source: 'url', url: 'git@github.com:example/codex plugins.git' }, { source: 'url', url: 'https://github.com/example/codex-plugins.git', sha: 'not-hex' }, { source: 'git-subdir', url: 'https://github.com/example/codex-plugins.git' }, + { path: './plugins/remote-helper', source: 'git-subdir', url: 'https://%zz/repository.git' }, + { package: '@example/codex-plugin', registry: 'https://%zz/', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://registry.example.test:port', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://registry.example.test/a b', source: 'npm' }, { package: '@example/codex-plugin', source: 'npm', version: 'file:../local' }, { package: '@example/codex-plugin', source: 'npm', version: 'https://example.test/pkg.tgz' }, { package: '@example/codex-plugin', source: 'npm', version: 'npm:other@1.0.0' }, @@ -226,9 +251,34 @@ it.each([ { code: 'codex.marketplace.policy.field.unknown', value: { policy: { approval: 'prompt' } } }, { code: 'codex.marketplace.policy.authentication.invalid', value: { policy: { authentication: 'ALWAYS' } } }, { code: 'codex.marketplace.policy.installation.invalid', value: { policy: { installation: 'HIDDEN' } } }, + { code: 'codex.marketplace.policy.installation.not-installable', value: { policy: { installation: 'NOT_AVAILABLE' } } }, ] as const)('rejects invalid authored Codex marketplace input with $code', ({ code, value }) => { const rejected = emittedMarketplace(withCodexConfig({ marketplace: value })); expect(rejected.codes).toContain(code); expect(rejected.document).toBeUndefined(); }); + +it('keeps NOT_AVAILABLE documented in the pinned schema but refuses it for the self-installing artifact', () => { + // Live codex-cli 0.147.0 registers a NOT_AVAILABLE marketplace entry and then refuses + // `codex plugin add @`, the exact command INSTALL.md and installBundle() run. + expect(codexCapabilityTable.marketplace.policy.notInstallable).toBe('NOT_AVAILABLE'); + expect(codexCapabilityTable.distribution.marketplacePolicy).toMatchObject({ + evidence: expect.arrayContaining([expect.stringContaining('is not available for install in marketplace')]), + rejected: ['NOT_AVAILABLE'], + }); + const validate = createAdapterValidator().compile(marketplaceSchema); + expect(validate({ + ...marketplace('./'), + plugins: [{ ...entry('./'), policy: { authentication: 'ON_INSTALL', installation: 'NOT_AVAILABLE' } }], + })).toBe(true); + + const rejected = emittedMarketplace(withCodexConfig({ marketplace: { policy: { installation: 'NOT_AVAILABLE' } } })); + expect(rejected.codes).toEqual(['codex.marketplace.policy.installation.not-installable']); + expect(rejected.document).toBeUndefined(); + for (const installation of ['AVAILABLE', 'INSTALLED_BY_DEFAULT']) { + const admitted = emittedMarketplace(withCodexConfig({ marketplace: { policy: { installation } } })); + expect(admitted.codes).toEqual([]); + expect(admitted.document).toMatchObject({ plugins: [{ policy: { authentication: 'ON_INSTALL', installation } }] }); + } +}); From 7e818b9420d253b683c7de64845efbf12376417e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:12:59 +0000 Subject: [PATCH 3/7] fix(codex): reject line terminators in contained paths and range-check URL ports in the pinned schemas The containment lookahead used `.*`, which does not cross line terminators while the trailing character class did, so `./x\n/../../outside` escaped the marketplace or plugin root. Component, interface-asset, and marketplace path patterns now reject control characters and Unicode line terminators. Git URL and npm registry ports are range-checked to 0-65535. Addresses the second-round Codex review threads on #392. --- .../src/adapters/schemas/codex/PROVENANCE.json | 12 ++++++------ .../adapters/schemas/codex/marketplace.schema.json | 6 +++--- .../src/adapters/schemas/codex/plugin.schema.json | 8 ++++---- .../agent-bundle/tests/adapter-metadata.test.ts | 4 ++-- .../agent-bundle/tests/codex-distribution.test.ts | 14 ++++++++++++++ packages/agent-bundle/tests/host-adapters.test.ts | 9 ++++++++- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index e158baed4..d3c8515c4 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -11,12 +11,12 @@ "Inline mcpServers values must be objects; inline hook documents use the same closed eleven-event, command-or-mcp_tool handler shape as hooks.schema.json.", "The closed interface object admits every documented install-surface field; brandColor requires a six-digit hexadecimal value, external links require http(s), asset paths must stay inside the plugin root, and screenshots must be ./assets/-relative PNG paths.", "The apps pointer is const-locked to ./.app.json, and app.schema.json requires a nonempty apps map whose entries carry exactly one nonempty registered-connection id.", - "Component and interface-asset path patterns treat backslashes as separators and reject them outright so Windows-form parent traversal cannot escape the plugin root; HTTP(S) URL scheme patterns are case-insensitive to match WHATWG URL protocol normalization." + "Component and interface-asset path patterns treat backslashes as separators and reject them outright so Windows-form parent traversal cannot escape the plugin root, and they reject control characters and Unicode line terminators (U+0000-U+001F, U+007F, U+2028, U+2029) so a line break cannot hide a parent segment from the containment lookahead; HTTP(S) URL scheme patterns are case-insensitive to match WHATWG URL protocol normalization." ], "marketplace.schema.json": [ "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", - "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid hostname (optional userinfo and port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions reject path and URL selectors, and registries must be credential-, query-, and fragment-free HTTPS URLs with the same hostname grammar.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid hostname (optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions reject path and URL selectors, and registries must be credential-, query-, and fragment-free HTTPS URLs with the same hostname grammar.", "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." ], "hooks.schema.json": [ @@ -37,8 +37,8 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 4213, - "sha256": "a4bca8063782e1bef29734f570c7e93b9c097f0a4dc676b6990f0a0122fd3b30", + "bytes": 4413, + "sha256": "4f643f986ce40e82fd545cd7d579e318a277c18418fa06eeccb76fb3447961a7", "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { @@ -47,8 +47,8 @@ "url": "https://github.com/openai/codex/blob/main/codex-rs/core/config.schema.json" }, "plugin.schema.json": { - "bytes": 6454, - "sha256": "986bcafa6ef46f9dc4558f05781f53400b3d75533a075068184ba8d43670d4ec", + "bytes": 6598, + "sha256": "4ad476545c96c83d899c4524dcccd4eb4fe7d3299c307878a0cd46a237f48d58", "url": "https://github.com/openai/codex/blob/main/codex-rs/skills/src/assets/samples/plugin-creator/references/plugin-json-spec.md" } }, diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index 3d10a394b..8295c2d06 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -3,12 +3,12 @@ "$id": "https://agent-bundle.dev/schemas/codex/0.147.0/marketplace.schema.json", "$defs": { "containedPath": { - "pattern": "^\\./(?!(?:.*[\\/\\\\])?\\.\\.(?:[\\/\\\\]|$))[^\\\\]*$", + "pattern": "^\\./(?!(?:.*[\\/\\\\])?\\.\\.(?:[\\/\\\\]|$))[^\\\\\\u0000-\\u001F\\u007F\\u2028\\u2029]*$", "type": "string" }, "gitUrl": { "minLength": 1, - "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::[0-9]{1,5})?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*:(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", + "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*:(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", "type": "string" }, "gitRef": { "minLength": 1, "type": "string" }, @@ -50,7 +50,7 @@ "properties": { "package": { "minLength": 1, "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$", "type": "string" }, "registry": { - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::[0-9]{1,5})?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", "type": "string" }, "source": { "const": "npm", "type": "string" }, diff --git a/packages/agent-bundle/src/adapters/schemas/codex/plugin.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/plugin.schema.json index 78e47aae4..c9403b984 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/plugin.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/plugin.schema.json @@ -3,7 +3,7 @@ "$id": "https://agent-bundle.dev/schemas/codex/0.147.0/plugin.schema.json", "$defs": { "componentPath": { - "pattern": "^\\./(?!(?:.*[\\/\\\\])?\\.\\.(?:[\\/\\\\]|$))[^\\\\]+$", + "pattern": "^\\./(?!(?:.*[\\/\\\\])?\\.\\.(?:[\\/\\\\]|$))[^\\\\\\u0000-\\u001F\\u007F\\u2028\\u2029]+$", "type": "string" }, "hookCommandHandler": { @@ -117,12 +117,12 @@ "brandColor": { "pattern": "^#[0-9A-Fa-f]{6}$", "type": "string" }, "capabilities": { "items": { "minLength": 1, "pattern": "\\S", "type": "string" }, "type": "array" }, "category": { "minLength": 1, "pattern": "\\S", "type": "string" }, - "composerIcon": { "pattern": "^\\./(?!(?:.*[/\\\\])?\\.\\.(?:[/\\\\]|$))[^\\\\]+$", "type": "string" }, + "composerIcon": { "pattern": "^\\./(?!(?:.*[/\\\\])?\\.\\.(?:[/\\\\]|$))[^\\\\\\u0000-\\u001F\\u007F\\u2028\\u2029]+$", "type": "string" }, "defaultPrompt": { "items": { "maxLength": 128, "minLength": 1, "pattern": "\\S", "type": "string" }, "maxItems": 3, "minItems": 1, "type": "array" }, "developerName": { "minLength": 1, "pattern": "\\S", "type": "string" }, "displayName": { "minLength": 1, "pattern": "\\S", "type": "string" }, - "logo": { "pattern": "^\\./(?!(?:.*[/\\\\])?\\.\\.(?:[/\\\\]|$))[^\\\\]+$", "type": "string" }, - "logoDark": { "pattern": "^\\./(?!(?:.*[/\\\\])?\\.\\.(?:[/\\\\]|$))[^\\\\]+$", "type": "string" }, + "logo": { "pattern": "^\\./(?!(?:.*[/\\\\])?\\.\\.(?:[/\\\\]|$))[^\\\\\\u0000-\\u001F\\u007F\\u2028\\u2029]+$", "type": "string" }, + "logoDark": { "pattern": "^\\./(?!(?:.*[/\\\\])?\\.\\.(?:[/\\\\]|$))[^\\\\\\u0000-\\u001F\\u007F\\u2028\\u2029]+$", "type": "string" }, "longDescription": { "minLength": 1, "pattern": "\\S", "type": "string" }, "privacyPolicyURL": { "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", "type": "string" }, "screenshots": { "items": { "pattern": "^\\./assets/(?!.*(?:^|/)\\.\\.(?:/|$)).+\\.png$", "type": "string" }, "type": "array" }, diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index 6836a7638..cc76f9f25 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: 'a4bca8063782e1bef29734f570c7e93b9c097f0a4dc676b6990f0a0122fd3b30', + sha256: '4f643f986ce40e82fd545cd7d579e318a277c18418fa06eeccb76fb3447961a7', }, { name: 'mcp', @@ -94,7 +94,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'plugin', revision: '0.147.0', - sha256: '986bcafa6ef46f9dc4558f05781f53400b3d75533a075068184ba8d43670d4ec', + sha256: '4ad476545c96c83d899c4524dcccd4eb4fe7d3299c307878a0cd46a237f48d58', }, ], }); diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts index 9b6930b48..ea70eb22e 100644 --- a/packages/agent-bundle/tests/codex-distribution.test.ts +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -146,6 +146,9 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'git@git.example.test:~team/codex-plugins.git' }, { source: 'url', url: 'https://github.com/example/codex%20plugins.git' }, { package: 'codex-plugin', registry: 'https://npm.example.test:4873/prefix/', source: 'npm' }, + { source: 'url', url: 'https://github.com:65535/example/codex-plugins.git' }, + { source: 'url', url: 'https://github.com:0443/example/codex-plugins.git' }, + { package: 'codex-plugin', registry: 'https://npm.example.test:65535', source: 'npm' }, ); expect(validate(admitted), JSON.stringify(validate.errors)).toBe(true); @@ -154,6 +157,13 @@ it('admits every documented marketplace source form and rejects escapes, credent 'plugins/my-plugin', { path: './plugins/../../outside', source: 'local' }, { path: '.\\plugins\\my-plugin', source: 'local' }, + // Line terminators must not let a parent segment slip past the containment lookahead. + './x\n/../../outside', + { path: './x\n/../../outside', source: 'local' }, + { path: './x\r\n/../../outside', source: 'local' }, + { path: './x\u2028/../../outside', source: 'local' }, + { path: './plugins/my\u0000plugin', source: 'local' }, + { path: './x\n/../../outside', ref: 'main', source: 'git-subdir', url: 'https://github.com/example/codex-plugins.git' }, { source: 'url', url: 'file:///tmp/plugins' }, // Scheme-prefix matches with an unusable authority or path are not URLs. { source: 'url', url: 'https://%zz/repository.git' }, @@ -163,6 +173,9 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'https://git hub.com/example/codex-plugins.git' }, { source: 'url', url: 'https://-github.com/example/codex-plugins.git' }, { source: 'url', url: 'https://github.com:port/example/codex-plugins.git' }, + { source: 'url', url: 'https://github.com:65536/example/codex-plugins.git' }, + { source: 'url', url: 'https://github.com:99999/example/codex-plugins.git' }, + { source: 'url', url: 'ssh://git@github.com:65536/example/codex-plugins.git' }, { source: 'url', url: 'https://github.com/example/codex-plugins.git?ref=main' }, { source: 'url', url: 'ssh://%zz/repository.git' }, { source: 'url', url: 'ssh://github.com' }, @@ -175,6 +188,7 @@ it('admits every documented marketplace source form and rejects escapes, credent { package: '@example/codex-plugin', registry: 'https://%zz/', source: 'npm' }, { package: '@example/codex-plugin', registry: 'https://', source: 'npm' }, { package: '@example/codex-plugin', registry: 'https://registry.example.test:port', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://registry.example.test:65536', source: 'npm' }, { package: '@example/codex-plugin', registry: 'https://registry.example.test/a b', source: 'npm' }, { package: '@example/codex-plugin', source: 'npm', version: 'file:../local' }, { package: '@example/codex-plugin', source: 'npm', version: 'https://example.test/pkg.tgz' }, diff --git a/packages/agent-bundle/tests/host-adapters.test.ts b/packages/agent-bundle/tests/host-adapters.test.ts index 234f3ebad..b5c7025d0 100644 --- a/packages/agent-bundle/tests/host-adapters.test.ts +++ b/packages/agent-bundle/tests/host-adapters.test.ts @@ -1163,8 +1163,15 @@ it('admits documented Codex component path and inline manifest forms', async () { mcpServers: { docs: 'not-an-object' }, skills: './skills/' }, { skills: '../skills/' }, { skills: ['./skills/'] }, + // Line terminators must not let a parent segment slip past the containment lookahead. + { skills: './x\n/../../outside/' }, + { hooks: './x\r\n/../../outside.json', skills: './skills/' }, + { mcpServers: './x\u2028/../../outside.json', skills: './skills/' }, + { skills: './skills\u0000/' }, + { interface: { ...manifest.interface, logo: './x\n/../../outside.png' }, skills: './skills/' }, + { interface: { ...manifest.interface, composerIcon: './x\u2029/../../outside.png' }, skills: './skills/' }, ]) { - expect(validate({ ...manifest, ...invalid })).toBe(false); + expect(validate({ ...manifest, ...invalid }), JSON.stringify(invalid)).toBe(false); } }); From 438b8ee9fee0ddbe013f7b9fcf433f461470ef07 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:21:29 +0000 Subject: [PATCH 4/7] fix(codex): range-check numeric IPv4 authorities and validate npm version selectors in the marketplace schema Hosts must be DNS labels whose last label contains a letter or an in-range dotted-quad IPv4, so https://999.999.999.999/repo.git no longer passes; npm `version` must be a semver version, a semver range, or a dist-tag composed only of characters encodeURIComponent leaves unescaped, matching the npm-package-arg rules that otherwise fail with EINVALIDTAGNAME. Addresses the third-round Codex review threads on #392. --- packages/agent-bundle/README.md | 3 ++- .../adapters/schemas/codex/PROVENANCE.json | 6 ++--- .../schemas/codex/marketplace.schema.json | 6 ++--- .../tests/adapter-metadata.test.ts | 2 +- .../tests/codex-distribution.test.ts | 22 +++++++++++++++++++ 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index 1b1631253..b62285806 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -67,7 +67,8 @@ values, except `installation: NOT_AVAILABLE`, which fails the build such entries and that is exactly the command the emitted `INSTALL.md` and `installBundle()` run. The pinned marketplace schema also admits Git root (`url`), `git-subdir`, and `npm` sources for validating real-world marketplaces (Git and registry URLs must carry a syntactically valid -hostname and path, not just a scheme prefix), but the adapter never emits them. Personal and legacy +host, port, and path rather than a bare scheme prefix, and npm `version` must be a semver +version, range, or dist-tag), but the adapter never emits them. Personal and legacy `.claude-plugin/marketplace.json` discovery, the `~/.codex/plugins/cache` layout, `config.toml` enable state, `features.plugins` / `features.hooks`, inline `[hooks]` TOML, `requirements.toml` managed hooks, `allow_managed_hooks_only`, and `restrict_to_allowed_sources` are host- or diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index d3c8515c4..0b8fdd405 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -16,7 +16,7 @@ "marketplace.schema.json": [ "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", - "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid hostname (optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions reject path and URL selectors, and registries must be credential-, query-, and fragment-free HTTPS URLs with the same hostname grammar.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, or an in-range dotted-quad IPv4; optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range, or a dist-tag made only of characters encodeURIComponent leaves unescaped (npm-package-arg rejects anything else with EINVALIDTAGNAME), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." ], "hooks.schema.json": [ @@ -37,8 +37,8 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 4413, - "sha256": "4f643f986ce40e82fd545cd7d579e318a277c18418fa06eeccb76fb3447961a7", + "bytes": 6399, + "sha256": "1f069283d53ea61be08375233c9ad7a805e318d4d229304cf4bec587f9557715", "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index 8295c2d06..cd716a151 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -8,7 +8,7 @@ }, "gitUrl": { "minLength": 1, - "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*:(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", + "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?):(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", "type": "string" }, "gitRef": { "minLength": 1, "type": "string" }, @@ -50,11 +50,11 @@ "properties": { "package": { "minLength": 1, "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$", "type": "string" }, "registry": { - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", "type": "string" }, "source": { "const": "npm", "type": "string" }, - "version": { "minLength": 1, "pattern": "^[^/\\\\:@]+$", "type": "string" } + "version": { "minLength": 1, "pattern": "^(?:(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)? - v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?: (?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*)(?: ?\\|\\| ?(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)? - v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?: (?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*))*|[A-Za-z0-9!'()*._~-]+)$", "type": "string" } }, "required": ["source", "package"], "type": "object" diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index cc76f9f25..08443474b 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: '4f643f986ce40e82fd545cd7d579e318a277c18418fa06eeccb76fb3447961a7', + sha256: '1f069283d53ea61be08375233c9ad7a805e318d4d229304cf4bec587f9557715', }, { name: 'mcp', diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts index ea70eb22e..91534459e 100644 --- a/packages/agent-bundle/tests/codex-distribution.test.ts +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -149,6 +149,15 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'https://github.com:65535/example/codex-plugins.git' }, { source: 'url', url: 'https://github.com:0443/example/codex-plugins.git' }, { package: 'codex-plugin', registry: 'https://npm.example.test:65535', source: 'npm' }, + { source: 'url', url: 'https://10.0.0.1/team/codex-plugins.git' }, + { source: 'url', url: 'https://192.168.1.250:8443/team/codex-plugins.git' }, + { source: 'url', url: 'git@10.0.0.1:team/codex-plugins.git' }, + { source: 'url', url: 'https://localhost/team/codex-plugins.git' }, + { source: 'url', url: 'https://xn--bcher-kva.example/team/codex-plugins.git' }, + { package: 'codex-plugin', registry: 'https://10.0.0.1:4873/', source: 'npm' }, + // npm version selectors: semver versions, ranges, and dist-tags (npm-package-arg rules). + ...['1.2.3', '~1.2', '>=1.0.0 <2.0.0', '1.2.3 - 2.3.4', '1.x', '*', '>=1.0.0-beta.1', '1.0.0+build.5', '^1 || ^2', 'v1.2.3', '=1.2.3', 'next', 'beta-2', 'rc.1'] + .map((version) => ({ package: 'codex-plugin', source: 'npm', version })), ); expect(validate(admitted), JSON.stringify(validate.errors)).toBe(true); @@ -189,6 +198,19 @@ it('admits every documented marketplace source form and rejects escapes, credent { package: '@example/codex-plugin', registry: 'https://', source: 'npm' }, { package: '@example/codex-plugin', registry: 'https://registry.example.test:port', source: 'npm' }, { package: '@example/codex-plugin', registry: 'https://registry.example.test:65536', source: 'npm' }, + // Numeric authorities must be in-range dotted-quad IPv4; a last label of digits is not a DNS host. + { source: 'url', url: 'https://999.999.999.999/team/plugin.git' }, + { source: 'url', url: 'https://256.1.1.1/team/plugin.git' }, + { source: 'url', url: 'https://1.2.3/team/plugin.git' }, + { source: 'url', url: 'https://1.2.3.4.5/team/plugin.git' }, + { source: 'url', url: 'https://example.123/team/plugin.git' }, + { source: 'url', url: 'git@999.999.999.999:team/plugin.git' }, + { source: 'url', url: 'ssh://999.999.999.999/team/plugin.git' }, + { package: '@example/codex-plugin', registry: 'https://999.999.999.999/', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://example.123/', source: 'npm' }, + // Selectors npm rejects with EINVALIDTAGNAME or as unparseable ranges. + ...['foo bar', '%', '', '1.2.3 || foo bar', '>=1.0.0 <', 'latest@1', 'a/b', 'a:b', 'a#b', 'a?b'] + .map((version) => ({ package: '@example/codex-plugin', source: 'npm', version })), { package: '@example/codex-plugin', registry: 'https://registry.example.test/a b', source: 'npm' }, { package: '@example/codex-plugin', source: 'npm', version: 'file:../local' }, { package: '@example/codex-plugin', source: 'npm', version: 'https://example.test/pkg.tgz' }, From 82c0c413bbab67ec3368bf7a0164341b04a66ea9 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:33:44 +0000 Subject: [PATCH 5/7] fix(codex): accept bracketed IPv6 authorities and semver whitespace in marketplace URL and version patterns Git URLs, scp-style remotes, and npm registries now admit RFC 3986 IP-literal hosts such as ssh://git@[2001:db8::1]/repo.git and https://[::1]:4873/, and npm `version` follows semver's whitespace grammar (any whitespace run between comparators or around a hyphen range, trimmed at the ends) instead of a single literal space. Addresses the fourth-round Codex review threads on #392. --- packages/agent-bundle/README.md | 4 +-- .../adapters/schemas/codex/PROVENANCE.json | 6 ++--- .../schemas/codex/marketplace.schema.json | 6 ++--- .../tests/adapter-metadata.test.ts | 2 +- .../tests/codex-distribution.test.ts | 25 ++++++++++++++++--- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index b62285806..cf56253f4 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -67,8 +67,8 @@ values, except `installation: NOT_AVAILABLE`, which fails the build such entries and that is exactly the command the emitted `INSTALL.md` and `installBundle()` run. The pinned marketplace schema also admits Git root (`url`), `git-subdir`, and `npm` sources for validating real-world marketplaces (Git and registry URLs must carry a syntactically valid -host, port, and path rather than a bare scheme prefix, and npm `version` must be a semver -version, range, or dist-tag), but the adapter never emits them. Personal and legacy +host (DNS, IPv4, or bracketed IPv6), port, and path rather than a bare scheme prefix, and +npm `version` must be a semver version, range, or dist-tag), but the adapter never emits them. Personal and legacy `.claude-plugin/marketplace.json` discovery, the `~/.codex/plugins/cache` layout, `config.toml` enable state, `features.plugins` / `features.hooks`, inline `[hooks]` TOML, `requirements.toml` managed hooks, `allow_managed_hooks_only`, and `restrict_to_allowed_sources` are host- or diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index 0b8fdd405..0b5624490 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -16,7 +16,7 @@ "marketplace.schema.json": [ "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", - "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, or an in-range dotted-quad IPv4; optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range, or a dist-tag made only of characters encodeURIComponent leaves unescaped (npm-package-arg rejects anything else with EINVALIDTAGNAME), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, an in-range dotted-quad IPv4, or a bracketed RFC 3986 IPv6 literal; optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range using semver's whitespace grammar (any whitespace run between comparators and around hyphens, trimmed at the ends), or a dist-tag made only of characters encodeURIComponent leaves unescaped (npm-package-arg rejects anything else with EINVALIDTAGNAME), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." ], "hooks.schema.json": [ @@ -37,8 +37,8 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 6399, - "sha256": "1f069283d53ea61be08375233c9ad7a805e318d4d229304cf4bec587f9557715", + "bytes": 10805, + "sha256": "e0002bb331bd79c3f948dcc33168eb5e2cbc1f76dd72ed04c711f45c48cb5e19", "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index cd716a151..f4251b4a0 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -8,7 +8,7 @@ }, "gitUrl": { "minLength": 1, - "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?):(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", + "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?::|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?):(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", "type": "string" }, "gitRef": { "minLength": 1, "type": "string" }, @@ -50,11 +50,11 @@ "properties": { "package": { "minLength": 1, "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$", "type": "string" }, "registry": { - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+)*/?$", "type": "string" }, "source": { "const": "npm", "type": "string" }, - "version": { "minLength": 1, "pattern": "^(?:(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)? - v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?: (?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*)(?: ?\\|\\| ?(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)? - v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?: (?:(?:[<>]=?|=|~>?|\\^) ?)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*))*|[A-Za-z0-9!'()*._~-]+)$", "type": "string" } + "version": { "minLength": 1, "pattern": "^(?:\\s*(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?\\s+-\\s+v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?:\\s+(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*)(?:\\s*\\|\\|\\s*(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?\\s+-\\s+v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?:\\s+(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*))*\\s*|[A-Za-z0-9!'()*._~-]+)$", "type": "string" } }, "required": ["source", "package"], "type": "object" diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index 08443474b..1e4e7b6ae 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: '1f069283d53ea61be08375233c9ad7a805e318d4d229304cf4bec587f9557715', + sha256: 'e0002bb331bd79c3f948dcc33168eb5e2cbc1f76dd72ed04c711f45c48cb5e19', }, { name: 'mcp', diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts index 91534459e..8d73d5a6b 100644 --- a/packages/agent-bundle/tests/codex-distribution.test.ts +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -155,8 +155,18 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'https://localhost/team/codex-plugins.git' }, { source: 'url', url: 'https://xn--bcher-kva.example/team/codex-plugins.git' }, { package: 'codex-plugin', registry: 'https://10.0.0.1:4873/', source: 'npm' }, - // npm version selectors: semver versions, ranges, and dist-tags (npm-package-arg rules). - ...['1.2.3', '~1.2', '>=1.0.0 <2.0.0', '1.2.3 - 2.3.4', '1.x', '*', '>=1.0.0-beta.1', '1.0.0+build.5', '^1 || ^2', 'v1.2.3', '=1.2.3', 'next', 'beta-2', 'rc.1'] + // Bracketed IPv6 authorities (RFC 3986 IP-literal) in every URL form Git and npm accept. + { source: 'url', url: 'ssh://git@[2001:db8::1]/team/codex-plugins.git' }, + { source: 'url', url: 'https://[2001:db8::1]:4873/team/codex-plugins.git' }, + { source: 'url', url: 'https://[::1]/team/codex-plugins.git' }, + { source: 'url', url: 'https://[::ffff:10.0.0.1]/team/codex-plugins.git' }, + { source: 'url', url: 'https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/team/codex-plugins.git' }, + { source: 'url', url: 'git@[2001:db8::1]:team/codex-plugins.git' }, + { package: 'codex-plugin', registry: 'https://[2001:db8::1]:4873/', source: 'npm' }, + { package: 'codex-plugin', registry: 'https://[::1]/', source: 'npm' }, + // npm version selectors: semver versions, ranges, and dist-tags (npm-package-arg rules), using + // semver's whitespace grammar (any run of whitespace between comparators, trimmed at the ends). + ...['1.2.3', '~1.2', '>=1.0.0 <2.0.0', '>=1.0.0 <2.0.0', '>=1.0.0\t<2.0.0', '>= 1.0.0', '~ 1.2', ' 1.2.3 ', '1.2.3 - 2.3.4', '1.0.0 - 2.0.0', '1.x', '*', '>=1.0.0-beta.1', '1.0.0+build.5', '^1 || ^2', '^1||^2', 'v1.2.3', '=1.2.3', 'next', 'beta-2', 'rc.1'] .map((version) => ({ package: 'codex-plugin', source: 'npm', version })), ); expect(validate(admitted), JSON.stringify(validate.errors)).toBe(true); @@ -208,8 +218,17 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'ssh://999.999.999.999/team/plugin.git' }, { package: '@example/codex-plugin', registry: 'https://999.999.999.999/', source: 'npm' }, { package: '@example/codex-plugin', registry: 'https://example.123/', source: 'npm' }, + // IPv6 literals must be bracketed, well-formed, and at most eight groups. + { source: 'url', url: 'https://[2001:db8::1/team/plugin.git' }, + { source: 'url', url: 'https://[zz::1]/team/plugin.git' }, + { source: 'url', url: 'https://[1:2:3:4:5:6:7:8:9]/team/plugin.git' }, + { source: 'url', url: 'https://[1:2:3:4:5:6:7]/team/plugin.git' }, + { source: 'url', url: 'https://[]/team/plugin.git' }, + { source: 'url', url: 'https://2001:db8::1/team/plugin.git' }, + { package: '@example/codex-plugin', registry: 'https://[2001:db8::1', source: 'npm' }, + { package: '@example/codex-plugin', registry: 'https://[1:2:3:4:5:6:7:8:9]/', source: 'npm' }, // Selectors npm rejects with EINVALIDTAGNAME or as unparseable ranges. - ...['foo bar', '%', '', '1.2.3 || foo bar', '>=1.0.0 <', 'latest@1', 'a/b', 'a:b', 'a#b', 'a?b'] + ...['foo bar', '%', '', '1.2.3 || foo bar', '>=1.0.0 <', 'latest@1', 'a/b', 'a:b', 'a#b', 'a?b', '1.0.0 -2.0.0'] .map((version) => ({ package: '@example/codex-plugin', source: 'npm', version })), { package: '@example/codex-plugin', registry: 'https://registry.example.test/a b', source: 'npm' }, { package: '@example/codex-plugin', source: 'npm', version: 'file:../local' }, From c395a7e4edab92c592683f6b3a0879e2a4c89361 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:44:05 +0000 Subject: [PATCH 6/7] fix(codex): apply git check-ref-format to marketplace refs and reject dot-prefixed npm dist-tags `ref` selectors now follow git check-ref-format (no control characters, whitespace, ~^:?*[\, "..", or "@{"; no empty, dot-leading, ".lock"- or dot-trailing components; no leading "-" or lone "@"), and npm `version` dist-tags may not start with "." because npm-package-arg classifies those as directory specs rather than registry selectors. Addresses the fifth-round Codex review threads on #392. --- packages/agent-bundle/README.md | 3 ++- .../src/adapters/schemas/codex/PROVENANCE.json | 6 +++--- .../adapters/schemas/codex/marketplace.schema.json | 4 ++-- packages/agent-bundle/tests/adapter-metadata.test.ts | 2 +- .../agent-bundle/tests/codex-distribution.test.ts | 11 +++++++++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/agent-bundle/README.md b/packages/agent-bundle/README.md index cf56253f4..85f601bad 100644 --- a/packages/agent-bundle/README.md +++ b/packages/agent-bundle/README.md @@ -68,7 +68,8 @@ such entries and that is exactly the command the emitted `INSTALL.md` and `insta The pinned marketplace schema also admits Git root (`url`), `git-subdir`, and `npm` sources for validating real-world marketplaces (Git and registry URLs must carry a syntactically valid host (DNS, IPv4, or bracketed IPv6), port, and path rather than a bare scheme prefix, and -npm `version` must be a semver version, range, or dist-tag), but the adapter never emits them. Personal and legacy +npm `version` must be a semver version, range, or dist-tag, and Git `ref` must satisfy +`git check-ref-format`), but the adapter never emits them. Personal and legacy `.claude-plugin/marketplace.json` discovery, the `~/.codex/plugins/cache` layout, `config.toml` enable state, `features.plugins` / `features.hooks`, inline `[hooks]` TOML, `requirements.toml` managed hooks, `allow_managed_hooks_only`, and `restrict_to_allowed_sources` are host- or diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index 0b5624490..2f7756afe 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -16,7 +16,7 @@ "marketplace.schema.json": [ "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", - "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, an in-range dotted-quad IPv4, or a bracketed RFC 3986 IPv6 literal; optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range using semver's whitespace grammar (any whitespace run between comparators and around hyphens, trimmed at the ends), or a dist-tag made only of characters encodeURIComponent leaves unescaped (npm-package-arg rejects anything else with EINVALIDTAGNAME), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, an in-range dotted-quad IPv4, or a bracketed RFC 3986 IPv6 literal; optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; ref selectors follow git check-ref-format (no control characters, whitespace, ~^:?*[\\, \"..\", or \"@{\"; components cannot be empty, start with \".\", or end with \".\" or \".lock\"; no leading \"-\" or lone \"@\"), sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range using semver's whitespace grammar (any whitespace run between comparators and around hyphens, trimmed at the ends), or a dist-tag made only of characters encodeURIComponent leaves unescaped and not starting with \".\" (npm-package-arg rejects anything else with EINVALIDTAGNAME or treats it as a directory spec), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." ], "hooks.schema.json": [ @@ -37,8 +37,8 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 10805, - "sha256": "e0002bb331bd79c3f948dcc33168eb5e2cbc1f76dd72ed04c711f45c48cb5e19", + "bytes": 11042, + "sha256": "2767d326838307d220ac7684d5400bc173d60d98d95087ad77af90fb8072c22a", "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index f4251b4a0..06c8c6024 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -11,7 +11,7 @@ "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?::|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?):(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", "type": "string" }, - "gitRef": { "minLength": 1, "type": "string" }, + "gitRef": { "minLength": 1, "pattern": "^(?!-)(?!@$)(?!\\.)(?![^/]*\\.lock(?:/|$))(?:[^\\u0000-\\u0020\\u007F~^:?*\\[\\\\/.@]|\\.(?![./]|$)|@(?!\\{))+(?:/(?!\\.)(?![^/]*\\.lock(?:/|$))(?:[^\\u0000-\\u0020\\u007F~^:?*\\[\\\\/.@]|\\.(?![./]|$)|@(?!\\{))+)*$", "type": "string" }, "gitSha": { "minLength": 1, "pattern": "^[0-9A-Fa-f]{7,64}$", "type": "string" }, "localSource": { "additionalProperties": false, @@ -54,7 +54,7 @@ "type": "string" }, "source": { "const": "npm", "type": "string" }, - "version": { "minLength": 1, "pattern": "^(?:\\s*(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?\\s+-\\s+v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?:\\s+(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*)(?:\\s*\\|\\|\\s*(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?\\s+-\\s+v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?:\\s+(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*))*\\s*|[A-Za-z0-9!'()*._~-]+)$", "type": "string" } + "version": { "minLength": 1, "pattern": "^(?:\\s*(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?\\s+-\\s+v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?:\\s+(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*)(?:\\s*\\|\\|\\s*(?:v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?\\s+-\\s+v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?|(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?(?:\\s+(?:(?:[<>]=?|=|~>?|\\^)\\s*)?v?(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:\\.(?:[xX*]|0|[1-9][0-9]*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?)?)?)*))*\\s*|(?!\\.)[A-Za-z0-9!'()*._~-]+)$", "type": "string" } }, "required": ["source", "package"], "type": "object" diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index 1e4e7b6ae..d919ee45d 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: 'e0002bb331bd79c3f948dcc33168eb5e2cbc1f76dd72ed04c711f45c48cb5e19', + sha256: '2767d326838307d220ac7684d5400bc173d60d98d95087ad77af90fb8072c22a', }, { name: 'mcp', diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts index 8d73d5a6b..398a07865 100644 --- a/packages/agent-bundle/tests/codex-distribution.test.ts +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -164,6 +164,9 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'git@[2001:db8::1]:team/codex-plugins.git' }, { package: 'codex-plugin', registry: 'https://[2001:db8::1]:4873/', source: 'npm' }, { package: 'codex-plugin', registry: 'https://[::1]/', source: 'npm' }, + // Git refs follow git check-ref-format: branch, tag, and fully qualified forms. + ...['release/1.2', 'v1.2.3', 'refs/tags/v1.2.3', 'feature/foo.bar', 'HEAD', 'user/-dash', 'x.lockfile', 'a@b'] + .map((ref) => ({ ref, source: 'url', url: 'https://github.com/example/codex-plugins.git' })), // npm version selectors: semver versions, ranges, and dist-tags (npm-package-arg rules), using // semver's whitespace grammar (any run of whitespace between comparators, trimmed at the ends). ...['1.2.3', '~1.2', '>=1.0.0 <2.0.0', '>=1.0.0 <2.0.0', '>=1.0.0\t<2.0.0', '>= 1.0.0', '~ 1.2', ' 1.2.3 ', '1.2.3 - 2.3.4', '1.0.0 - 2.0.0', '1.x', '*', '>=1.0.0-beta.1', '1.0.0+build.5', '^1 || ^2', '^1||^2', 'v1.2.3', '=1.2.3', 'next', 'beta-2', 'rc.1'] @@ -230,6 +233,14 @@ it('admits every documented marketplace source form and rejects escapes, credent // Selectors npm rejects with EINVALIDTAGNAME or as unparseable ranges. ...['foo bar', '%', '', '1.2.3 || foo bar', '>=1.0.0 <', 'latest@1', 'a/b', 'a:b', 'a#b', 'a?b', '1.0.0 -2.0.0'] .map((version) => ({ package: '@example/codex-plugin', source: 'npm', version })), + // Dot-prefixed selectors are directory specs to npm-package-arg, not dist-tags. + ...['.', '..', '.foo', '.latest'].map((version) => ({ package: '@example/codex-plugin', source: 'npm', version })), + // Refs git check-ref-format refuses (control characters, "..", "@{", ".lock", leading "-", empty components). + ...['', 'foo bar', 'foo..bar', 'foo@{1}', '@', '-main', 'main.lock', 'a.lock/b', 'refs/heads/', '/main', 'a//b', 'main.', '.hidden', 'a/.b', 'a~b', 'a^b', 'a:b', 'a?b', 'a*b', 'a[b', 'a\\b', 'a\tb', 'a\u007fb', 'a\nb'] + .flatMap((ref) => [ + { ref, source: 'url', url: 'https://github.com/example/codex-plugins.git' }, + { path: './plugins/remote-helper', ref, source: 'git-subdir', url: 'https://github.com/example/codex-plugins.git' }, + ]), { package: '@example/codex-plugin', registry: 'https://registry.example.test/a b', source: 'npm' }, { package: '@example/codex-plugin', source: 'npm', version: 'file:../local' }, { package: '@example/codex-plugin', source: 'npm', version: 'https://example.test/pkg.tgz' }, From e6297110d80880dc15954c55d308860d46ce9666 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 07:54:00 +0000 Subject: [PATCH 7/7] fix(codex): accept RFC 3986 userinfo (user:password, percent-escapes) in marketplace Git URLs Credentialed Git sources such as https://user:token@git.example.test/repo.git are valid to Git and were already half-supported (bare token@ passed); the userinfo grammar now matches RFC 3986 while still refusing a masked bad authority, raw spaces, malformed escapes, or a repeated "@". Addresses the sixth-round Codex review thread on #392. --- .../src/adapters/schemas/codex/PROVENANCE.json | 6 +++--- .../adapters/schemas/codex/marketplace.schema.json | 2 +- .../agent-bundle/tests/adapter-metadata.test.ts | 2 +- .../agent-bundle/tests/codex-distribution.test.ts | 13 +++++++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json index 2f7756afe..6aff5440e 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/PROVENANCE.json @@ -16,7 +16,7 @@ "marketplace.schema.json": [ "Transcribed 2026-09-02 from the Marketplace metadata section of https://developers.openai.com/plugins/build/plugins: top-level name, interface.displayName, and plugins[] entries with name, source, policy, and category.", "interface is required even though one documented example omits it: the adapter always emits a displayName, and the marketplace picker shows that title.", - "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, an in-range dotted-quad IPv4, or a bracketed RFC 3986 IPv6 literal; optional userinfo and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; ref selectors follow git check-ref-format (no control characters, whitespace, ~^:?*[\\, \"..\", or \"@{\"; components cannot be empty, start with \".\", or end with \".\" or \".lock\"; no leading \"-\" or lone \"@\"), sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range using semver's whitespace grammar (any whitespace run between comparators and around hyphens, trimmed at the ends), or a dist-tag made only of characters encodeURIComponent leaves unescaped and not starting with \".\" (npm-package-arg rejects anything else with EINVALIDTAGNAME or treats it as a directory spec), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", + "source admits every documented form: a plain ./-prefixed string, { source: local, path }, { source: url, url, ref?, sha? }, { source: git-subdir, url, path, ref?, sha? }, and { source: npm, package, version?, registry? }; paths are root-contained with backslashes, control characters, and Unicode line terminators rejected, Git URLs must be HTTP(S) or ssh:// with a syntactically valid host (DNS labels whose last label contains a letter, an in-range dotted-quad IPv4, or a bracketed RFC 3986 IPv6 literal; optional RFC 3986 userinfo including user:password and percent-escapes, and a 0-65535 port) and a non-empty percent-encoded repository path, or scp-style user@host:path, so scheme-prefixed values with an unusable authority such as https://%zz/repo.git or https://999.999.999.999/repo.git are rejected; ref selectors follow git check-ref-format (no control characters, whitespace, ~^:?*[\\, \"..\", or \"@{\"; components cannot be empty, start with \".\", or end with \".\" or \".lock\"; no leading \"-\" or lone \"@\"), sha selectors are 7-64 hexadecimal digits, npm versions must be a semver version, a semver range using semver's whitespace grammar (any whitespace run between comparators and around hyphens, trimmed at the ends), or a dist-tag made only of characters encodeURIComponent leaves unescaped and not starting with \".\" (npm-package-arg rejects anything else with EINVALIDTAGNAME or treats it as a directory spec), and registries must be credential-, query-, and fragment-free HTTPS URLs with the same host grammar.", "policy.installation and policy.authentication are closed to the documented AVAILABLE|INSTALLED_BY_DEFAULT|NOT_AVAILABLE and ON_INSTALL|ON_USE values, and both are required alongside category as the page instructs." ], "hooks.schema.json": [ @@ -37,8 +37,8 @@ "url": "https://learn.chatgpt.com/docs/hooks" }, "marketplace.schema.json": { - "bytes": 11042, - "sha256": "2767d326838307d220ac7684d5400bc173d60d98d95087ad77af90fb8072c22a", + "bytes": 11063, + "sha256": "fbccce3ade39e1b077fcb440e60260de6e811e8c2d222b2ae0fe8fe47706b470", "url": "https://developers.openai.com/plugins/build/plugins" }, "mcp.schema.json": { diff --git a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json index 06c8c6024..628e7519b 100644 --- a/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json +++ b/packages/agent-bundle/src/adapters/schemas/codex/marketplace.schema.json @@ -8,7 +8,7 @@ }, "gitUrl": { "minLength": 1, - "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:[A-Za-z0-9._~!$&'()*+,;=-]+@)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?::|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?):(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", + "pattern": "^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ss][Ss][Hh])://(?:(?:[A-Za-z0-9._~!$&'()*+,;=:-]|%[0-9A-Fa-f]{2})+@)?(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?:[:/]|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?::0*(?:0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})+)+/?|[A-Za-z0-9._-]+@(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|\\[(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)\\]|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*(?=[A-Za-z0-9-]{1,63}(?::|$))(?=[0-9-]*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?):(?:~?[A-Za-z0-9._!$&'()*+,;=@-]+)(?:/[A-Za-z0-9._~!$&'()*+,;=@-]+)*/?)$", "type": "string" }, "gitRef": { "minLength": 1, "pattern": "^(?!-)(?!@$)(?!\\.)(?![^/]*\\.lock(?:/|$))(?:[^\\u0000-\\u0020\\u007F~^:?*\\[\\\\/.@]|\\.(?![./]|$)|@(?!\\{))+(?:/(?!\\.)(?![^/]*\\.lock(?:/|$))(?:[^\\u0000-\\u0020\\u007F~^:?*\\[\\\\/.@]|\\.(?![./]|$)|@(?!\\{))+)*$", "type": "string" }, diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index d919ee45d..344805341 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -84,7 +84,7 @@ it('records exact immutable metadata for every built-in target', () => { { name: 'marketplace', revision: '0.147.0', - sha256: '2767d326838307d220ac7684d5400bc173d60d98d95087ad77af90fb8072c22a', + sha256: 'fbccce3ade39e1b077fcb440e60260de6e811e8c2d222b2ae0fe8fe47706b470', }, { name: 'mcp', diff --git a/packages/agent-bundle/tests/codex-distribution.test.ts b/packages/agent-bundle/tests/codex-distribution.test.ts index 398a07865..996ba3e3e 100644 --- a/packages/agent-bundle/tests/codex-distribution.test.ts +++ b/packages/agent-bundle/tests/codex-distribution.test.ts @@ -142,6 +142,11 @@ it('admits every documented marketplace source form and rejects escapes, credent { package: 'codex-plugin', source: 'npm' }, { package: 'codex-plugin', source: 'npm', version: 'latest' }, { source: 'url', url: 'https://token@git.example.test:8443/team/codex-plugins.git' }, + // RFC 3986 userinfo: user:password and percent-encoded credentials, on every host form. + { source: 'url', url: 'https://user:token@git.example.test/team/codex-plugins.git' }, + { source: 'url', url: 'https://user%40corp:p%3Ass@git.example.test/team/codex-plugins.git' }, + { source: 'url', url: 'https://user:token@10.0.0.1:8443/team/codex-plugins.git' }, + { source: 'url', url: 'https://user:token@[2001:db8::1]:8443/team/codex-plugins.git' }, { source: 'url', url: 'ssh://git@git.example.test:2222/team/codex-plugins.git' }, { source: 'url', url: 'git@git.example.test:~team/codex-plugins.git' }, { source: 'url', url: 'https://github.com/example/codex%20plugins.git' }, @@ -204,6 +209,14 @@ it('admits every documented marketplace source form and rejects escapes, credent { source: 'url', url: 'git@%zz:example/codex-plugins.git' }, { source: 'url', url: 'git@github.com:' }, { source: 'url', url: 'git@github.com:example/codex plugins.git' }, + // Credentials still cannot mask a bad authority, contain raw spaces or bad escapes, or repeat "@". + { source: 'url', url: 'https://user:token@/team/codex-plugins.git' }, + { source: 'url', url: 'https://user:to ken@git.example.test/team/codex-plugins.git' }, + { source: 'url', url: 'https://us%zzer@git.example.test/team/codex-plugins.git' }, + { source: 'url', url: 'https://user:token@%zz/team/codex-plugins.git' }, + { source: 'url', url: 'https://user:token@github.com:65536/team/codex-plugins.git' }, + { source: 'url', url: 'https://user:token@999.999.999.999/team/codex-plugins.git' }, + { source: 'url', url: 'https://a@b@github.com/team/codex-plugins.git' }, { source: 'url', url: 'https://github.com/example/codex-plugins.git', sha: 'not-hex' }, { source: 'git-subdir', url: 'https://github.com/example/codex-plugins.git' }, { path: './plugins/remote-helper', source: 'git-subdir', url: 'https://%zz/repository.git' },