Skip to content

Add Codex plugin marketplace support#407

Merged
hbrodin merged 3 commits into
mainfrom
claude/codex-plugin-marketplace-af1142
Jul 14, 2026
Merged

Add Codex plugin marketplace support#407
hbrodin merged 3 commits into
mainfrom
claude/codex-plugin-marketplace-af1142

Conversation

@frabert

@frabert frabert commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What

Adds first-class support for Codex plugins and marketplaces, mirroring the existing Claude Code mechanism.

  • New [codex] marketplaces / [codex] plugins config fields (with ~ expansion and local-path validation), documented in config.example.toml, docs/codex-integration.md, and docs/configuration.md.
  • Baked into the golden image on the Lima backend (recorded in TemplateConfig as codex_marketplaces/codex_plugins, staleness-checked in needs_rebuild) and delta-installed on first boot via codex plugin marketplace add / codex plugin add. On Firecracker nothing is baked, so the full set installs on first boot — identical to how Claude already behaves there.
  • Profiles stay Claude-only; Codex plugins are config-level [codex].

Why the config.toml handling is the tricky part

The Codex CLI stores marketplace registrations ([marketplaces.*]) and per-plugin enabled state ([plugins.*]) in ~/.codex/config.toml — the same file coop rewrites from the host on every boot. A naive port would wipe those tables on every restart (the bug class already fixed for Claude's settings.json).

So coop now reads the guest's config.toml back and preserves its [marketplaces.*]/[plugins.*] tables across the rewrite, while dropping any that came from the host's own config.toml (host state must not leak into the guest). Installed plugins — and manual /plugins enable/disable toggles — survive stop/start. The guest read is gated on whether a rewrite will actually happen (no wasted SSH round-trip otherwise) and warns rather than silently dropping state when the guest config can't be read or parsed, mirroring merge_managed_claude_settings.

Local marketplace directories are copied into a per-tool guest subdir (~/.coop/marketplaces/{claude,codex}/…) so same-basename marketplaces from the two agents don't overwrite each other.

Reviewer notes

  • Behavior change to call out: marketplaces/plugins a user set directly in their host ~/.codex/config.toml (rather than in coop's [codex]) are now stripped from the guest, where previously the host config was copied verbatim. This is intentional (declarative [codex] is the source of truth; don't leak host state), but it changes existing behavior.
  • Shell-out installers live in backend.rs/lima.rs, which are whole-module mutation-excluded, so no .cargo/mutants.toml change was needed. The pure helpers that landed in swept modules (collect_codex_baked_lists, check_local_marketplaces) are unit-tested, and compute_codex_plugin_delta's field wiring has a direct test despite backend.rs being excluded.
  • The codex plugin ... CLI shape differs from Claude's (add not install; no --scope/-s user), so it needs its own installer functions — the shared local-dir copy logic is factored into stage_marketplace_source.

Testing

  • cargo fmt / cargo clippy --all-targets clean; 996 unit tests pass (new tests cover config parse/validate/tilde-expansion, config.toml preservation, host-state dropping, extract_codex_plugin_state round-trip + error cases, plugin_delta, the baked-list collector, and compute_codex_plugin_delta field wiring).
  • cargo mutants --in-diff on the changed mutation-swept logic: 16/16 caught, 0 missed.
  • Not yet run — needs a VM environment: integration tests on both platforms. Suggested manual check: set [codex] marketplaces/plugins, coop up, verify codex plugin list in-guest, then coop stop + coop start and confirm the plugin survives (exercises the preservation path).

🤖 Generated with Claude Code

@hbrodin hbrodin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed for correctness, standards adherence, and user-facing behavior. Builds clean (clippy -D warnings), 1003 unit tests pass, and I adversarially re-checked my initial concerns. Two of them collapsed on verification: the [plugins.<name>] enabled config shape is real (not fabricated) and extract_codex_plugin_state clones the whole plugins/marketplaces subtree, so preservation is shape-agnostic; and the missing-CHANGELOG concern was wrong (this repo batches CHANGELOG entries at release-prep, not per-PR). What remains are the three non-blocking nits below. The main thing still worth doing before merge is the manual stop/start plugin-survival check the PR description already lists, since the integration path is untested against a real Codex CLI.

Comment thread src/backend.rs
},
Err(e) => {
tracing::warn!(
"Could not read guest ~/.codex/config.toml to preserve installed Codex \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking consistency note: plugin/marketplace install is BootMode::FirstBoot-only, so on a restart, if this read (or the parse below) fails while a config.toml rewrite is otherwise triggered, the guest's installed [marketplaces.*]/[plugins.*] are dropped with just this warning and no reinstall to recover them. This matches the accepted merge_managed_claude_settings tradeoff, so it's consistent — but note the realistic Err here is a transient SSH read of an otherwise-good file, not the corrupt file that path deliberately resets. The Claude path carries a comment addressing exactly this; a one-line note here pointing at the FirstBoot-only recovery would match it.

Comment thread src/backend.rs Outdated
[marketplaces.mine]\n\
source = \"trailofbits/codex-plugins\"\n\
\n\
[plugins.my-lsp]\n\

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: a real Codex install keys this table by plugin@marketplace (e.g. [plugins."my-lsp@codex-plugins"], matching the plugins = ["my-lsp@codex-plugins"] field), not the bare [plugins.my-lsp]. The extractor clones the whole plugins subtree so behavior is unaffected, but the quoted key would make the fixture reflect actual guest output.

Comment thread docs/codex-integration.md Outdated

### Plugin marketplaces

`marketplaces` and `plugins` declare Codex [plugin marketplaces](https://developers.openai.com/codex/plugins) and the plugins to install from them, mirroring the same fields under `[claude]`:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this URL 308-redirects cross-host to learn.chatgpt.com/docs/plugins. It works today, but linking the canonical host directly avoids future link rot.

@frabert

frabert commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

I've tested the plugins in Codex under a Lima machine, but haven't tested a Firecracker VM

@hbrodin

hbrodin commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

running test on linux

@hbrodin

hbrodin commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Integration test results — Linux / Firecracker

Ran the full suite on a Firecracker host (the "not yet run" item from the PR description).

  • Host: <firecracker-host> — Ubuntu, kernel 6.17.0-23-generic, x86_64, OpenSSH 9.6p1
  • Binary: cross-compiled x86_64-unknown-linux-musl from this branch (fb1a05f)
  • Command: ./tests/run-integration.sh --remote <firecracker-host> --full

Result

337 passed, 3 failed, 6 skipped

The 6 skips are all expected (Firecracker one-time rsync mounts, macOS-only AppleDouble check, destroy --all gated behind COOP_TEST_DESTRUCTIVE, disk-GiB not reported by status).

The 3 failures — stale test assertions, not a product bug

All three are in the pre-existing local marketplace phase of tests/integration.sh, and all three stem from this PR's own change: local marketplaces are now copied into a per-tool subdir (~/.coop/marketplaces/{claude,codex}/…), but the integration test still asserts the old flat layout (~/.coop/marketplaces/<name>). This PR does not touch tests/integration.sh, so the test was never updated for the new layout.

PASS  up with local marketplace exits 0
FAIL  marketplace manifest copied to guest
      got: ''
FAIL  plugin skill file copied recursively
      got: ''
PASS  claude plugin marketplace add was invoked
FAIL  marketplace add uses guest path (not host path)
      log: 'plugin marketplace add ~/.coop/marketplaces/claude/test-marketplace --scope user'

The add-command log is the giveaway: the command correctly uses ~/.coop/marketplaces/**claude**/test-marketplace, while the test greps for coop/marketplaces/test-marketplace (no claude/) and reads the manifest from $HOME/.coop/marketplaces/test-marketplace/….

I ruled out the scp tilde-expansion gotcha (documented in CLAUDE.md) as a cause: on this host's OpenSSH 9.6, scp -r src host:~/dir/leaf expands the leading ~ correctly and lands the file in the real $HOME (verified directly). The copy path (GUEST_MARKETPLACE_DIR = "~/.coop/marketplaces") is pre-existing on main and works. The feature's happy path is fine — up succeeds, the marketplace is copied, and marketplace add is invoked with the correct guest path.

Suggested fix (test-only)

Update the marketplace phase in tests/integration.sh for the per-tool subdir:

  • Line ~3560: local mp_base="\$HOME/.coop/marketplaces/$mp_name"…/marketplaces/claude/$mp_name
  • Line ~3615: grep -q "coop/marketplaces/$mp_name"grep -q "coop/marketplaces/claude/$mp_name"

With those two edits the phase should pass; no product-code change is needed. Happy to push this to the branch if you'd like.

@hbrodin

hbrodin commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Re-run after 2ac28d3 — green

Re-ran the full suite on the same Firecracker host against the updated branch (2ac28d3, which fixes the stale marketplace test paths).

  • Command: ./tests/run-integration.sh --remote <firecracker-host> --full
340 passed, 0 failed, 6 skipped

The 3 previously-failing assertions in the local marketplace copy phase now pass:

PASS  marketplace manifest copied to guest
PASS  plugin skill file copied recursively
PASS  marketplace add uses guest path (not host path)

6 skips unchanged and all expected (Firecracker one-time rsync mounts, macOS-only AppleDouble check, destroy --all gated behind COOP_TEST_DESTRUCTIVE, disk-GiB not reported by status). Firecracker/Linux integration is clean.

@hbrodin

hbrodin commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Thanks @frabert ! I've rerun it on firecracker and it seems to be working.

frabert and others added 3 commits July 14, 2026 12:32
Mirror the existing Claude plugin/marketplace mechanism for Codex:
`[codex] marketplaces` / `plugins` are baked into the golden image on
the Lima backend and delta-installed on first boot via `codex plugin
marketplace add` / `codex plugin add` (Firecracker bakes nothing and
installs the full set on first boot, as Claude already does there).

The Codex CLI stores marketplace registrations (`[marketplaces.*]`) and
per-plugin enabled state (`[plugins.*]`) in `~/.codex/config.toml` — the
same file coop rewrites on every boot. coop now reads the guest's config
back and preserves those tables across the rewrite (dropping any that
came from the host's own config.toml so host state does not leak in), so
installed plugins and manual `/plugins` toggles survive stop/start. The
guest read is gated on whether a rewrite will actually happen, and warns
rather than silently dropping state when the guest config cannot be read
or parsed.

Local marketplace directories are copied into a per-tool guest subdir
(`~/.coop/marketplaces/{claude,codex}/…`) so same-basename marketplaces
from the two agents do not collide.

Profiles stay Claude-only; Codex plugins are config-level `[codex]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- read_codex_plugin_state: note that the realistic failure is a transient
  SSH read (not the corrupt-file reset the Claude path does), and that
  because install is FirstBoot-only a restart won't reinstall dropped
  tables — recovery requires recreating the instance.
- Tests: key the preservation fixtures by real guest output —
  `[marketplaces.<name>]` and the quoted `[plugins."plugin@marketplace"]` —
  instead of the bare `[plugins.my-lsp]` placeholder.
- docs/codex-integration.md: link the canonical plugins doc host
  (learn.chatgpt.com) directly instead of the developers.openai.com URL
  that 308-redirects to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The collision fix moved local marketplaces into a per-tool guest subdir
(`~/.coop/marketplaces/{claude,codex}/…`), but the `local marketplace`
phase of tests/integration.sh still asserted the old flat layout,
failing 3 assertions on the Firecracker/Linux run. This is a `[claude]`
marketplace, so update the copy-verification base path and the
Firecracker `marketplace add` log grep to include the `claude/` subdir.

Test-only; no product-code change. Reported by @hbrodin's Firecracker
integration run on this branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hbrodin hbrodin force-pushed the claude/codex-plugin-marketplace-af1142 branch from 2ac28d3 to 5c39069 Compare July 14, 2026 10:32

@hbrodin hbrodin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@hbrodin hbrodin merged commit 8b742e1 into main Jul 14, 2026
7 checks passed
@hbrodin hbrodin deleted the claude/codex-plugin-marketplace-af1142 branch July 14, 2026 10:39
@hbrodin hbrodin mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants