Skip to content

feat(plugins): wire extraKnownMarketplaces + enabledPlugins via container.json#2365

Open
yaniv-golan wants to merge 2 commits into
nanocoai:mainfrom
yaniv-golan:pr/plugin-marketplaces
Open

feat(plugins): wire extraKnownMarketplaces + enabledPlugins via container.json#2365
yaniv-golan wants to merge 2 commits into
nanocoai:mainfrom
yaniv-golan:pr/plugin-marketplaces

Conversation

@yaniv-golan
Copy link
Copy Markdown
Contributor

Summary

Add per-group plugin support via the SDK's settings-driven install path. Operators declare marketplaces + enabled plugins in groups/<folder>/container.json:plugins; the host mirrors them into per-group settings.json (extraKnownMarketplaces / enabledPlugins) on every spawn, and the container env gets CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 + CLAUDE_CODE_REMOTE=1 (gated on plugins being non-empty).

Also lays the foundation for safe concurrent writes to container.json (needed by the per-group operator skills in a follow-up PR).

What's new

Host:

  • ContainerConfig.plugins field with the SDK's typed extraKnownMarketplaces source schema (8 variants) and enabledPlugins value union (boolean | string[] | object).
  • New updateContainerConfig(folder, mutate) async helper. Atomic write-then-rename in writeContainerConfig. Advisory file lock (60s stale TTL, jittered polling). Existing applyInstallPackages / applyAddMcpServer callers routed through the locked path; ensureRuntimeFields and per-group imageTag write also locked.
  • ensurePluginsConfig() in group-init mirrors container.json:plugins into settings.json on every spawn. Defensive against malformed/non-object/stale-format settings.json. Additive merge (entries already present in settings.json that aren't in container.json are preserved).
  • container-runner sets CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 + CLAUDE_CODE_REMOTE=1 when plugins block is non-empty. CLAUDE_CODE_REMOTE is mandatory — without it, github source URLs default to SSH and bypass the OneCLI gateway entirely.
  • Drive-by fix bundled: GIT_SSL_CAINFO is now set to mirror NODE_EXTRA_CA_CERTS. OneCLI's applyContainerConfig() injects the latter for various language runtimes but not for git, so the SDK's marketplace clones (which shell out to git clone) failed with "server certificate verification failed" through the gateway. With the new env, git trusts the OneCLI MITM cert and clones complete normally.

Container:

  • Provider event types: new plugin_install_failed variant on ProviderEvent.
  • claude.ts:translateEvents() handles SDK plugin_install system messages — failed status emits the new event, others are activity-only.
  • poll-loop.ts:handleEvent logs plugin_install_failed events for operator visibility.

Tests

  • 12 new vitest cases in src/container-config.test.ts and src/group-init.test.ts:
    • Atomic write (no .tmp.* leftovers after rename)
    • 10 concurrent writers serialized via the lock (vitest spawns in parallel)
    • Lock release on mutator throw (no deadlock)
    • Defensive parsing: malformed JSON, non-object top level, missing blocks
    • First-init with plugins pre-declared in container.json
    • Idempotent re-merge
    • Additive merge preserves pre-existing settings.json entries
    • container.json wins on key collision
    • Plugins-config round-trip through write+read
  • Vitest config exclude pattern added so **/container/** (which uses bun:test) doesn't get picked up by host vitest under v4's auto-workspace discovery.

How operators use it

Without follow-up operator skills, an operator can already use this PR by hand-editing groups/<folder>/container.json:

{
  "plugins": {
    "marketplaces": {
      "anthropic": {
        "source": { "source": "github", "repo": "anthropics/skills", "ref": "main" }
      }
    },
    "enabled": { "doc-coauthoring@anthropic": true }
  }
}

Restart the group; the SDK installs the marketplace + plugins at next session init.

Empirical verification

End-to-end: container respawned after declaring xiaolai-marketplace in container.json:plugins. plugin_install:installed fires inside the container; SDK clones marketplace into ~/.claude/plugins/marketplaces/xiaolai/; installed_plugins.json and known_marketplaces.json both written by SDK as expected; agent's init event lists the new codex-toolkit plugin.

For the failure path: declaring a private repo without the corresponding OneCLI vault github auth produces a plugin_install:failed event with a clear "marketplace.json not found" or "authentication failed" message — surfaced via the new plugin_install_failed provider event.

Test plan

  • pnpm test (host) passes — 435 tests, including 12 new ones for this change
  • bun run typecheck (container) clean
  • End-to-end: container spawns with new env, SDK installs marketplace, plugin loads in agent's init event
  • Failure mode: clone fails → plugin_install_failed event → operator sees error in host log

🤖 Generated with Claude Code

yaniv-golan and others added 2 commits May 9, 2026 16:46
…iner.json

Per-group plugin support via the SDK's settings-driven install path.
Operators declare marketplaces + enabled plugins in
groups/<folder>/container.json:plugins; group-init mirrors them into
per-group settings.json on every spawn (additive merge), and
container-runner sets CLAUDE_CODE_SYNC_PLUGIN_INSTALL=1 +
CLAUDE_CODE_REMOTE=1 when plugins are declared. CLAUDE_CODE_REMOTE is
mandatory — without it, github source URLs default to SSH and bypass
OneCLI's HTTPS proxy entirely.

Also adds the container.json concurrency primitive that subsequent
plugin/marketplace operator skills will need: writeContainerConfig is
now atomic (write-then-rename), updateContainerConfig acquires an
advisory file lock with 60s stale TTL. Existing read-modify-write
callers (self-mod apply.ts handlers, container-runner's
ensureRuntimeFields and per-agent-group imageTag persistence) all
routed through the locked path.

Container side: claude provider's translateEvents() now handles SDK
plugin_install system messages — `failed` status emits a new
plugin_install_failed provider event so install errors surface in host
logs; started/installed/completed are activity-only. New event variant
added to ProviderEvent in types.ts; poll-loop logs it.

Tests: 12 new vitest cases — atomic writes, concurrent writers (10
parallel updaters), lock release on throw, defensive parsing of
stale-format settings.json (malformed JSON, non-object top level,
missing blocks), plugins config round-trip, additive merge,
key-collision precedence, idempotency.

vitest.config.ts: exclude container/ so vitest 4's auto-workspace
discovery doesn't pick up the bun:test files in agent-runner — drive-by
fix needed for the new tests to run from the host root.

Pre-PR empirical test report (Q1, Q2, Q4, Q5, Q9 all confirmed
positively) lives at docs/internal/plugin-install-empirical-test.md
(gitignored, fork-internal).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
OneCLI's applyContainerConfig() injects NODE_EXTRA_CA_CERTS,
SSL_CERT_FILE, and DENO_CERT for various language runtimes — but
not GIT_SSL_CAINFO. Without it, git's HTTPS verification falls back
to the system CA bundle, doesn't trust the OneCLI MITM cert, and
fails every clone through the gateway with:

  fatal: unable to access 'https://github.com/...':
  server certificate verification failed. CAfile: none

Symptom (caught during PR 1 end-to-end test): SDK marketplace clone
fired but failed even for public github repos because HTTPS through
the proxy couldn't verify the cert; SDK then fell back to SSH which
also failed (no agent forwarding in container) and surfaced as a
plugin_install:failed event.

Fix: post-process the args after applyContainerConfig — find the
NODE_EXTRA_CA_CERTS=<path> entry it just added, mirror the same path
into GIT_SSL_CAINFO. Same path, both env vars, git happy.

Manual public-repo HTTPS clone inside the container now succeeds.
SDK plugin install path will work for public repos without further
config; private repos still need the OneCLI vault entry from
/setup-private-plugins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant