SC-5 Plugin + marketplace + MCP swap (single source of truth in DB) #627

Closed
opened 2026-05-01 10:33:59 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a platform engineer, I want agent-env-sync.renderForInstance to be the only writer of enabledPlugins, extraKnownMarketplaces, and mcpServers keys in each per-agent env dir, so that agent-type and per-instance overrides land coherently and agents.json::plugins + the per-env .claude.json / settings.json stop being mutable surfaces.

Acceptance criteria

Renderer is the only writer

  • renderForInstance (SC-2) is the only code path that writes settings.json::enabledPlugins, settings.json::extraKnownMarketplaces, and .claude.json::mcpServers. Existing writers (e.g. one-shot bootstrap calls in agent-env-sync and containers-rebuild) delegate to it.
  • agents.json::types[].plugins[] becomes a builtin-only input. builtin-sync reads it and writes one plugin_binding row per (type, plugin) pair under scope = 'builtin'.
  • config/mcp-builtin.json is read by builtin-sync and produces mcp_server rows.
  • config/marketplaces-builtin.json produces plugin_marketplace rows.

Container rebuild integration

  • just containers-rebuild [NAME] triggers a renderForInstance for every affected instance after the rebuild, so the new container boots with up-to-date plugin / marketplace / MCP state.
  • container-reconcile.ts learns to delegate drift recovery to renderForInstance instead of mutating settings.json directly.

Secret refs

  • MCP server env rows can carry ${SECRET:NAME} placeholders. renderForInstance resolves them via the secrets table (SC-6) and writes plaintext only to the rendered .claude.json on disk.
  • A missing secret produces MissingSecretError per SC-2's error contract.

Tests

  • Unit: an instance-scope plugin_binding row enabling a plugin not on the type appears in the instance's rendered enabledPlugins.
  • Unit: an instance-scope plugin_binding with enabled = false removes a plugin that the type enables.
  • Unit: mcp_server row with env = { "FORGEJO_TOKEN": "${SECRET:FORGEJO_TOKEN_DEV}" } resolves to the decrypted token at render time.
  • Integration: just containers-rebuild dev-default produces a freshly-rendered env dir.

Out of scope

  • Master-key encryption rotation (separate ticket).
  • HTTP routes / UI (SC-7+).
  • Hooks (excluded — see spec Non-goals).

References

  • specs/agent-config-customization.md §Story SC-5
  • apps/server/src/infrastructure/agent-env-sync/ — current materialiser
  • apps/server/src/infrastructure/container/container-reconcile.ts and container-watchdog.ts
  • ~/.config/claude-hooks/agent-env/<agent>/settings.json and .claude.json — the rendered files
  • Depends on SC-1, SC-2, SC-6.
## User story As a platform engineer, I want `agent-env-sync.renderForInstance` to be the only writer of `enabledPlugins`, `extraKnownMarketplaces`, and `mcpServers` keys in each per-agent env dir, so that agent-type and per-instance overrides land coherently and `agents.json::plugins` + the per-env `.claude.json` / `settings.json` stop being mutable surfaces. ## Acceptance criteria ### Renderer is the only writer - [ ] `renderForInstance` (SC-2) is the only code path that writes `settings.json::enabledPlugins`, `settings.json::extraKnownMarketplaces`, and `.claude.json::mcpServers`. Existing writers (e.g. one-shot bootstrap calls in `agent-env-sync` and `containers-rebuild`) delegate to it. - [ ] `agents.json::types[].plugins[]` becomes a builtin-only input. `builtin-sync` reads it and writes one `plugin_binding` row per `(type, plugin)` pair under `scope = 'builtin'`. - [ ] `config/mcp-builtin.json` is read by `builtin-sync` and produces `mcp_server` rows. - [ ] `config/marketplaces-builtin.json` produces `plugin_marketplace` rows. ### Container rebuild integration - [ ] `just containers-rebuild [NAME]` triggers a `renderForInstance` for every affected instance after the rebuild, so the new container boots with up-to-date plugin / marketplace / MCP state. - [ ] `container-reconcile.ts` learns to delegate drift recovery to `renderForInstance` instead of mutating `settings.json` directly. ### Secret refs - [ ] MCP server `env` rows can carry `${SECRET:NAME}` placeholders. `renderForInstance` resolves them via the secrets table (SC-6) and writes plaintext only to the rendered `.claude.json` on disk. - [ ] A missing secret produces `MissingSecretError` per SC-2's error contract. ### Tests - [ ] Unit: an `instance`-scope `plugin_binding` row enabling a plugin not on the type appears in the instance's rendered `enabledPlugins`. - [ ] Unit: an `instance`-scope `plugin_binding` with `enabled = false` removes a plugin that the type enables. - [ ] Unit: `mcp_server` row with `env = { "FORGEJO_TOKEN": "${SECRET:FORGEJO_TOKEN_DEV}" }` resolves to the decrypted token at render time. - [ ] Integration: `just containers-rebuild dev-default` produces a freshly-rendered env dir. ## Out of scope - Master-key encryption rotation (separate ticket). - HTTP routes / UI (SC-7+). - Hooks (excluded — see spec Non-goals). ## References - `specs/agent-config-customization.md` §Story SC-5 - `apps/server/src/infrastructure/agent-env-sync/` — current materialiser - `apps/server/src/infrastructure/container/container-reconcile.ts` and `container-watchdog.ts` - `~/.config/claude-hooks/agent-env/<agent>/settings.json` and `.claude.json` — the rendered files - Depends on **SC-1**, **SC-2**, **SC-6**.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#627
No description provided.