diff --git a/docs/harness-plugin-layouts.md b/docs/harness-plugin-layouts.md index a9f6c28..4b89763 100644 --- a/docs/harness-plugin-layouts.md +++ b/docs/harness-plugin-layouts.md @@ -54,9 +54,9 @@ Source: https://github.com/agentplugins/agent-plugins-spec/blob/main/spec/1.0.0. - MCP loads **only** from root `mcp.json` — inline config / alternate paths forbidden. - Skill discovery is **non-recursive** (immediate children of `skills/`). -> The spec requires `$schema`, but our **shipped** `plugin.json` currently omits -> it so Codex routes to `.codex-plugin/` (see [Codex](#codex)). It goes back once -> Codex can pass user config through the spec. +> `$schema` is required, and `validate` enforces it. It was omitted for a while so +> Codex would route to `.codex-plugin/`; Codex 0.150.0 removed that need (see +> [Codex](#codex)). --- @@ -107,11 +107,14 @@ environment variables to an MCP server: `env_vars` is rejected, `${VAR}` in `env is not expanded, and the ambient env is cleared ([openai/codex#36854](https://github.com/openai/codex/issues/36854)). So we generate Codex's **legacy** `.codex-plugin/` layout in every case, which does -forward `env_vars`. +forward `env_vars`. Since +[0.150.0](https://github.com/openai/codex/commit/40b7560169c7274147a47f9b0c75db89fe016d34) +Codex reads the spec manifest and *overlays* those `env_vars` onto servers of the +same name, so both files are used together. ``` / -├── plugin.json # spec manifest, but WITHOUT $schema (see below) +├── plugin.json # spec manifest, with $schema ├── .codex-plugin/ │ ├── plugin.json # legacy manifest; mcpServers -> "./.codex-plugin/.mcp.json" │ └── .mcp.json # legacy MCP: command/args + env_vars (forwarded from user env) @@ -119,17 +122,20 @@ forward `env_vars`. └── /SKILL.md ``` -- **Omit `$schema` from the root `plugin.json`.** Codex treats a root manifest as - Agent Plugin only when its `$schema` is an `agent-plugins.org` URI; without it, - Codex falls through to `.codex-plugin/`. +- **Keep `$schema` in the root `plugin.json`.** Codex treats a root manifest as + Agent Plugin only when its `$schema` is an `agent-plugins.org` URI. It then takes + the `env_vars` from `.codex-plugin/` and applies them to the matching spec + servers, so server names must match across `mcp.json` and + `.codex-plugin/.mcp.json`. **Requires Codex 0.150.0+**; older versions ignore the + overlay, leaving user config unforwarded. - The `mcpServers` path in `.codex-plugin/plugin.json` is **root-relative** (`./.codex-plugin/.mcp.json`), not relative to `.codex-plugin/`. - `.codex-plugin/.mcp.json` uses `env_vars: [...]` so the user's environment reaches the server, the capability the spec `mcp.json` lacks. - Install/discovery still needs a marketplace descriptor (`.claude-plugin/marketplace.json`). -> When Codex can pass user config through the spec, add `$schema` back and drop -> this generator; Codex (and the other spec-only clients) then read the spec directly. +> Once the spec itself can forward user env vars, drop this generator; Codex and +> the other spec clients then need nothing but `plugin.json` + `mcp.json`. --- diff --git a/src/agent_plugin_sync/generators/codex.py b/src/agent_plugin_sync/generators/codex.py index 80ac054..5c3a36f 100644 --- a/src/agent_plugin_sync/generators/codex.py +++ b/src/agent_plugin_sync/generators/codex.py @@ -2,10 +2,11 @@ Codex reads the Agent Plugin spec, but its spec `mcp.json` cannot forward user environment variables to an MCP server (`env_vars` is rejected, `${VAR}` in `env` -is not expanded, ambient env is cleared). So we target Codex's **legacy** +is not expanded, ambient env is cleared). So we also emit Codex's **legacy** `.codex-plugin/` format, whose `env_vars` list forwards named vars from the user -environment. For Codex to use these files the root `plugin.json` must omit -`$schema`; see docs/harness-plugin-layouts.md. +environment. Codex 0.150.0+ overlays those `env_vars` onto the spec servers of the +same name, so the root `plugin.json` keeps its `$schema`; see +docs/harness-plugin-layouts.md. Config vars (`com.google.cloud.data.agent-plugins.config`) become `env_vars` on each server. The manifest references the sibling MCP file with a **root-relative** diff --git a/src/agent_plugin_sync/validate.py b/src/agent_plugin_sync/validate.py index 7a9ea97..ef46fac 100644 --- a/src/agent_plugin_sync/validate.py +++ b/src/agent_plugin_sync/validate.py @@ -36,15 +36,16 @@ def validate_plugin(root: pathlib.Path) -> ValidationResult: # Malformed top level; can't check the bucket meaningfully after this. return ValidationResult(ok=False, errors=_format(e, "plugin.json")) - # Codex reads a root manifest as Agent Plugin (and so uses the spec mcp.json, - # which cannot forward user env vars) only when its $schema is an agent-plugins - # URI. We route Codex to the generated .codex-plugin/ instead, so $schema must - # be absent; with it present, that generated file is silently ignored. - schema = raw.get("$schema", "") - if isinstance(schema, str) and schema.startswith(_AGENT_PLUGIN_SCHEMA_PREFIX): + # A spec client recognises the manifest by its $schema, so it must be an + # agent-plugins URI. Codex 0.150.0 and later still forward user env vars in that + # case: they overlay the generated .codex-plugin/ env_vars onto the spec servers + # (openai/codex#40363). Older Codex ignores that overlay, so plugins targeting + # 0.149.x and earlier must stay on an older release of this tool. + schema = raw.get("$schema") + if not (isinstance(schema, str) and schema.startswith(_AGENT_PLUGIN_SCHEMA_PREFIX)): errors.append( - "plugin.json: omit $schema; with it, Codex reads the spec mcp.json and ignores " - "the generated .codex-plugin/ (its env_vars)" + f"plugin.json: $schema must be an Agent Plugin spec URI ({_AGENT_PLUGIN_SCHEMA_PREFIX}...); " + "without it a spec client does not recognise the manifest" ) bucket = plugin.extensions.get(agent_plugin_sync.PLUGIN_EXTENSION_NS, {}) diff --git a/tests/conftest.py b/tests/conftest.py index bb5ca1d..a5b5150 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,7 +32,7 @@ def _make( codex: dict | None = None, mcp: dict | None = None, skills: bool = False, - schema: bool = False, + schema: bool = True, ) -> pathlib.Path: root = pathlib.Path(root) ns: dict = {} @@ -43,8 +43,8 @@ def _make( if codex is not None: ns["codex"] = codex - # $schema is omitted by default: with it, Codex ignores the generated - # .codex-plugin/. `schema=True` opts in to exercise that validation error. + # $schema is present by default: a spec client needs it to recognise the + # manifest. `schema=False` opts out to exercise that validation error. plugin: dict = { "name": name, "version": "0.1.0", diff --git a/tests/test_validate.py b/tests/test_validate.py index 57dc670..c5dc147 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -21,11 +21,11 @@ def test_accepts_a_well_formed_source(self, make_plugin, tmp_path): # Assert assert result.ok, result.errors - def test_rejects_agent_plugin_schema_on_root_manifest(self, make_plugin, tmp_path): - """A root plugin.json $schema routes Codex to the spec mcp.json, silently - ignoring the generated .codex-plugin/, so it must be omitted.""" + def test_rejects_root_manifest_without_agent_plugin_schema(self, make_plugin, tmp_path): + """Without an agent-plugins $schema a spec client does not recognise the + manifest, so it is required.""" # Arrange - root = make_plugin(tmp_path, schema=True) + root = make_plugin(tmp_path, schema=False) # Act result = validate.validate_plugin(root) diff --git a/uv.lock b/uv.lock index e55c2c6..a4cbca2 100644 --- a/uv.lock +++ b/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "agent-plugin-sync" -version = "0.1.0" +version = "0.1.2" source = { editable = "." } dependencies = [ { name = "pydantic" },