Skip to content

🤖 feat: enable Agent Plugin MCP servers globally - #4246

Merged
ThomasK33 merged 7 commits into
mainfrom
global-plugin-mcp-enablement
Sep 14, 2026
Merged

ThomasK33 merged 7 commits into
mainfrom
global-plugin-mcp-enablement

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 13, 2026

Copy link
Copy Markdown
Member

Summary

Allow globally installed Agent Plugin MCP servers to be enabled in Settings → MCP, rather than requiring a separate opt-in in every workspace. Plugin definitions stay read-only; only canonical server keys are persisted in enabledPluginServers.

  1. Apply global enablement to discovered global plugin servers. Workspace enable/disable overrides still take precedence; repo plugins and off-host restrictions remain unchanged.
  2. Serialize global MCP config mutations and fence startup, tool, and prompt admission with the same cross-process writer lock. Preserve unrelated JSONC bytes for plugin toggles and reject malformed or ambiguous enablement documents.
  3. Revoke global enablement during managed uninstall and before a fresh install reuses a plugin path. Roll back the installation or retain its recovery journal on failure; never restore consent as compensation. Settings backup restore holds the same lock across local planning and persistence, and strips plugin enablement so backups cannot grant machine-local execution consent.
  4. Enable the existing Settings switch, cover successful toggles and optimistic error rollback, and update the docs and bundled offline snapshot.

Validation

  • Rebased onto main at fbbea2b16403ceb6f52f71af33a13a06c9c466fa.
  • make static-check-full passed, including typecheck, lint, formatting, documentation links, and the lockfile-free CLI/bench gate.
  • 947 backend tests passed across MCP configuration, runtime, workspace overrides, plugin discovery, installation, and settings backup services. New regressions cover manual deletion/reinstall, failed consent cleanup, backup injection and command approval, redacted consent, and duplicate fields. Regression coverage includes held-tool/prompt revocation, startup revocation across all transports and override paths, fallback admission, workspace precedence, valid empty/comment-only config, malformed/unreadable consent, contention, cancellation, and lock cleanup.
  • 15 MCP Settings Storybook tests passed on b19f8dacf, including the new success and backend-error cases. The final backend-only update is covered by current-head CI.
  • Post-rebase recorded UAT passed on ee2bd3f8c (before the backend-only review fixes): global persistence, desktop/390px controls, workspace precedence, actual request tool lists, and managed component consent/uninstall/reinstall. Screenshots and five recordings.

Real-process revocation UAT passed on a0469f006: two independent Bun backends shared one disposable home and real stdio MCP transport. A held tool was rejected after sibling disable without another MCP call; explicit workspace consent and re-enablement both worked without re-serving the held tool. This tests the service/process/transport boundary, not the Settings UI or AI request assembly. Screenshots and real-time recording.

Startup/config UAT passed on 1e4c33acb: A completed global disable before B released its captured enabled snapshot. B then produced zero MCP startup, initialization, or call markers. Explicit workspace and re-enabled global startup/calls succeeded, and comments-only enable/disable/prune preserved comments. Screenshots and real-time recording.

Restore/fence UAT passed on 21118f908: actual restore queued behind a held real admission fence without changing consent. After release, restore removed consent, the next admission rejected, and fresh explicit enablement permitted admission again. This tests concurrent real service operations in one isolated Bun process, not full UI/AI/off-host behavior. Screenshot and recording. Regressions also cover fresh local-only merge planning and approval/write failure cleanup.

The final static/backend gate covers 21118f908; the review fixes do not change the UI.

Semantics and risks

Catalog and connection changes apply on the next workspace serve refresh. Global-plugin startup rechecks consent through process/connection initiation. Previously issued tools and prompts recheck at dispatch; already-admitted calls may finish. This does not promise immediate process termination, and explicit workspace enablement still overrides the global default. If an uninstall fails after revocation, the installation is recovered but the user may need to enable its server again. Restoring mcp.jsonc from a settings backup clears global plugin enablement; enable servers again in Settings → MCP. Tests cover concurrent writers, stale-read/prune ordering, malformed/duplicate JSONC, and rollback/recovery paths.


📋 Implementation Plan

The accepted plan is reproduced verbatim below. Its status notes and line references predate implementation; current validation is recorded above.

Allow enabling Agent Plugin MCP servers globally (Settings → MCP)

Problem

In Settings → MCP, rows for Agent Plugin servers (e.g. fux-plugins/coder, badge plugin · .xum/plugins/fux-plugins) have a disabled toggle with tooltip "Agent Plugin servers are enabled per workspace (Workspace MCP)". Plugin servers can only be enabled one workspace at a time via the Workspace MCP dialog. The user wants to enable them for all (eligible) workspaces from the global settings.

Verified facts the design rests on

  • Plugin servers are discovered fresh on every listing (no cache) from <xumHome>/plugins, ~/.agents/plugins (sourceScope: "global") and <repo>/.xum/plugins, <repo>/.agents/plugins ("project") by loadPluginMcpServers (src/node/services/agentPlugins/mcpConfig.ts), keyed plugin:<16-hex instanceId>:<serverName> and hard-coded disabled: true (~:466, ~:529). Global instance IDs hash the install path → stable across projects; computePluginSourceLocation (:269) renders .xum/plugins/<dir> for ~/.xum/plugins/<dir>, matching the screenshot.
  • The global Settings view calls api.mcp.list({}) (no projectPath) → MCPConfigService.listServerLayers scans global containers only, so only global plugins are ever offered there (src/node/services/mcpConfigService.ts:512-560).
  • listServerLayers merges {...plugin, ...global, ...project}; omitReservedPluginKeys (:44) strips plugin:* keys from user layers and addServer rejects them (:573): a plugin server's definition must never come from mcp.jsonc.
  • setServerEnabled (:625-648) only edits cfg.servers[name]; plugin keys fail "not found". UI mirrors this with disabled={isPluginEntry} (src/browser/features/Settings/Sections/MCPSettingsSection.tsx:1195-1228). Edit/remove (:1369) and tool-allowlist (:1450) editors are already hidden for plugin entries.
  • MCPConfigService has no write serialization: getGlobalConfig → mutate → saveGlobalConfig (:417-466, writeFileAtomic, JSON.stringify({servers}), re-normalizes and drops unknown top-level keys) can interleave across concurrent calls and processes. readConfigFile (:379) uses jsonc.parse(raw) without an errors array and returns {servers:{}} on any failure (startup-safe).
  • Multiple processes share one XUM_ROOT (desktop app + xum server, ALLOW_MULTIPLE_INSTANCES; mcpServerManager.ts:1050). The repo's pattern for this is acquireCrossProcessLock (src/node/utils/main/crossProcessLock.ts:286), used by WorkspaceMcpOverridesService.runExclusive (workspaceMcpOverridesService.ts:500-535: in-process queue + <xumHome>/mcp-overrides.lock, held across "revision read, validation, write, and prune" precisely to stop a save from re-enabling a server after an uninstall's prune) and by AgentPluginInstallService.runExclusive (installService.ts:769-790, cross-process MUTATION_LOCK_FILE).
  • Runtime (mcpServerManager.ts): every serve re-reads config from disk (ensureWorkspaceServers :1758 → getAllServers :1558 → configService.listServers), recomputes the enabled set via applyServerOverrides (:1625) + policy filter, and rebuilds the workspace's instances when the start-config signature changes (:1844-1870). Global toggles are therefore applied on the next serve, disk-authoritative, in every process; the router's mcp.setEnabled makes no manager call (router.ts:1020-1025). Off-host runtimes never get plugin servers (:1822-1830). pluginInvalidation (:1057, :1183-1220) retires plugin instances when a sibling process's on-disk mutation token changes, and the in-memory override caches (latestWorkspaceOverrides, loadFirstServeWorkspaceOverrides :1317) concern workspace overrides only — global config is never cached.
  • Uninstall (installService.ts:2798-3160): under runExclusive, pre-commit checks → stopServersWithKeyPrefix → journal → renameIntoStaging(targetPath, trashDir) (tree gone from discovery) → optional data staging → registry commit with pessimistic tombstones → post-commit workspace-override prunes. restoreTree(context) (:2896) rolls the tree back on pre-commit failures.

Design

Persist a global enable list — never plugin definitions — in ~/.xum/mcp.jsonc, applied only to discovered global-scope plugin servers:

{
  "servers": { /* unchanged */ },
  "enabledPluginServers": ["plugin:0123456789abcdef:coder"]
}

Effective precedence: plugin default (disabled: true) < global enabledPluginServers < workspace enabledServers / disabledServers. Applying the list inside the plugin layer means every consumer (Settings list, Workspace MCP modal, bun run debug plugins, tool assembly) sees the same effective default; the modal already handles "enabled by default → per-workspace disable" (WorkspaceMCPModal.tsx:122-158).

All mcp.jsonc writers (existing 4 mutations + the new list update + the uninstall prune) are serialized through a new MCPConfigService.runExclusive (in-process queue + cross-process <xumHome>/mcp-config.lock), mirroring WorkspaceMcpOverridesService. Lock contract is explicit and non-reentrant by convention (same as the overrides service): every public mutation/prune acquires runExclusive exactly once; private *Locked helpers never acquire it (documented in their doc comments; a concurrent second request queues, it never fails). The plugin toggle re-discovers the server inside the lock, and the uninstall prunes after the tree is staged out, so no previously persisted or already-validated enable survives a successful uninstall (analysis below).

Guarantee scope (stated precisely): no enable that was persisted, or whose in-lock validation observed the old installation, survives a successful uninstall. A toggle whose validation first runs after a same-name reinstall targets the current installation and is honored — rejecting it would need an install-incarnation contract, which is out of scope.

Race analysis (why lock + ordering closes it by construction)

Let T = toggle (setServerEnabled on a plugin key), E = ordinary mcp.jsonc mutation (whole-file RMW), U = uninstall (stage-outprune).

  • T holds the lock across discover → write. If T's scan precedes U's stage-out, the key is written before T releases; U's prune (needs the lock) runs after and removes it. If T's scan follows stage-out, the plugin is not discovered → Err("not found"), nothing written.
  • E holds the lock across read → write, so it either completes before the prune (which then removes the key) or reads the already-pruned file. Without the lock, E could read pre-prune and write post-prune, resurrecting the key — this is the interleaving the lock exists for (same rationale as workspaceMcpOverridesService.ts:500-511).
  • Cross-process: the lock is cross-process; the manager never caches global config, so sibling processes see the pruned list on their next serve, and pluginInvalidation retires their running plugin instances.
  • Failure ordering in U: the prune runs after the tree is staged out and before plugin-data staging, so its rollback is single-resource: prune failure → restoreTree; journal consumed only if the restore succeeded (otherwise the journal stays and reconcileJournals restores the tree later) → throw. What is promised is installation rollback/recovery, not consent: the prune's write may have landed before a later error (e.g. lock release), so consent may remain revoked and the user re-toggles. Old enablement is never restored as compensation. Any failure after a successful prune (data staging, registry commit) rolls the tree back via the existing paths with the same consequence: an inconvenience, never a silent enable. Crash between stage-out and prune → journal recovery restores the tree; consent still present for a still-installed plugin (consistent).
  • Duplicate-property trap (workspaceMcpOverridesService.ts:928-935): jsonc.parse exposes the LAST duplicate of a property while jsonc.modify edits the FIRST, so a file with two top-level enabledPluginServers properties could be "pruned" while its effective list is unchanged. Mutations therefore reject documents with a duplicated top-level enabledPluginServers and verify the edited document's effective list equals the intended list before writing; lenient reads grant nothing when the property is duplicated or malformed.
  • Discovery inside the lock is a plain fs scan (createAgentPluginsMcpProvider, no cache; core.ts:327) and claudeDesign.serverInfo(); neither takes a lock → no re-entrancy/deadlock. U holds the installer lock then takes the config lock; no config-lock holder ever takes the installer lock → no lock-order inversion.
Rejected alternatives
  • Partial servers["plugin:…"] = { disabled: false } entries: breaks the reserved-key design (omitReservedPluginKeys, addServer rejection, uninstall prune-by-shape); normalizeEntry needs a command/url.
  • Enablement in the install registry (plugins.json): only managed installs; drop-in plugins in ~/.xum/plugins / ~/.agents/plugins are discovered by directory scan.
  • Project-level (.xum/mcp.jsonc) list / project-scope plugins: not requested; would let a repo auto-enable stdio commands for contributors. readConfigFile parses the field for both files but only the global file's list is applied, and only to sourceScope === "global" entries.
  • Post-commit prune with durable tombstone: reuses the workspace-prune machinery but needs tombstone integration for a single local file; pre-commit-after-stage-out under the shared lock gives the same guarantee with a rollback path and far less code.
  • Pre-stage prune: leaves a window where T rediscovers the still-installed tree after the prune (fixable only by holding the config lock across the installer's rename → layering violation).

Kept intact: agent-plugins experiment gate (provider is only wired when enabled, core.ts:327-331), transport policy (setEnabledForApi, filterServersByPolicy), project trust, reserved namespace, off-host exclusion, global plugin tool-allowlist editing stays unsupported (workspace allowlists unchanged), no project-level enable. Runtime semantics match ordinary global servers: changes take effect at the next workspace catalog/serve refresh; no immediate process termination is promised (active leases may keep a server alive until its lease ends).

Implementation (one PR, three commits with gates) — est. ~+150 net LoC product code (range 130–180; excludes tests and dogfood fixtures)

Review status: design reviewed with the advisor over four rounds and approved as an implementation plan; implementation, validation and dogfooding are pending. No PR is created as part of this work unless the user asks.

Commit 1 — Backend: serialized config writes, enable list, precedence, uninstall prune (~+150 LoC)

  1. src/common/types/mcp.ts:66MCPConfig gains enabledPluginServers: string[] (normalized in-memory form; optional on disk). Only mcpConfigService.ts constructs this type (MCPConfig in di/ is an unrelated service tag).
  2. src/node/services/mcpConfigService.ts
    • runExclusive(fn) (private): in-process promise queue + acquireCrossProcessLock({ lockPath: <xumHome>/mcp-config.lock, acquireTimeoutMs: 60_000, staleMs: 5*60_000, timeoutMessage: "Another Mux process is currently updating MCP settings…" }), copied verbatim in shape from workspaceMcpOverridesService.ts:514-535 (no reentrancy framework, no depth assertions — a second request queues). Each public mutation acquires it exactly once: addServer (:561), setServerEnabled (:625), removeServer (:650), setToolAllowlist (:665) become try { return await this.runExclusive(async () => { …existing body… }) } catch (e) { return Err(getErrorMessage(e)) } so lock-acquisition failures (timeout) also surface as Result, never as throws. Reads stay lock-free.
    • readConfigFile (:379): lenient, non-crashing. servers handling unchanged, but the early !parsed.servers → return { servers: {} } guard (:389) must not short-circuit the enablement read: a field-only write to a previously missing file legitimately yields { "enabledPluginServers": [...] } with no servers key, so read the list independently. For enabledPluginServers: jsonc.parse(raw, errors) — if errors.length > 0 (partial tree) → []; else jsonc.parseTree and if the top-level property is duplicated (findDuplicateProperty, below) or present-but-not-an-array → [] (syntax-invalid or ambiguous config grants no plugin enablement); else keep only strings passing isCanonicalPluginServerKey.
    • saveGlobalConfig (:417-466): carry the list through: JSON.stringify({ servers: output, ...(cfg.enabledPluginServers.length ? { enabledPluginServers: cfg.enabledPluginServers } : {}) }) so ordinary mutations don't drop it.
    • updateEnabledPluginServersLocked(update: (current: string[]) => string[]): Promise<void> (private, throws; doc comment: "caller must hold runExclusive; never acquires"): read raw file (ENOENT → "{}"); strict jsonc.parse(raw, errors) → throw on errors.length > 0 or non-object root; jsonc.parseTree → throw if top-level enabledPluginServers is duplicated (workspaceMcpOverridesService.ts:928-935 trap) or present but not an array (checked before any equality/no-op logic); absent → []; extract current canonical list; next = update(current); return without writing when equal; else field-only patch jsonc.applyEdits(raw, jsonc.modify(raw, ["enabledPluginServers"], next.length ? next : undefined, { formattingOptions })); verify the edited text parses without errors and its effective enabledPluginServers (absent ⇒ [], which is how removal of the final key is verified) deep-equals next (repo lesson: validate after every jsonc.modify); writeFileAtomic(..., { mode: 0o600 }); bump globalConfigGeneration. Preserves comments/unknown fields; never re-serializes servers.
    • Shared helper: extract the inner duplicateIn of findDuplicateOverrideProperty (workspaceMcpOverridesService.ts:937-955) into src/node/utils/main/jsoncDuplicates.ts as findDuplicateProperty(node, names?); the overrides service calls it (mechanical, behavior-preserving) and mcpConfigService.ts reuses it (DRY per AGENTS.md).
    • listServerLayers (:520-534): after globalCfg is read, build a new plugin map: for each key in globalCfg.enabledPluginServers, if pluginServers[key]?.plugin?.sourceScope === "global", copy with disabled: false. Never mutates the provider's map; unmatched keys are ignored (cannot inject or touch user servers). Update the class doc comment (:64-69) — plugin definitions are never persisted; only global enablement keys are.
    • setServerEnabled (:625-648), body inside runExclusive (outer try/catch → Result as above): reuse managed = (await this.listServers())[name] (in-lock discovery). if (managed?.plugin): assert(isCanonicalPluginServerKey(name)); if (managed.plugin.sourceScope !== "global") return Err("Repo plugin servers are enabled per workspace"); await this.updateEnabledPluginServersLocked(list => enabled ? uniq([...list, name]) : list.filter(k => k !== name)); return Ok(). Non-plugin path unchanged; addServer/removeServer/setToolAllowlist keep rejecting plugin keys.
    • pruneEnabledPluginServers(keyPrefix) (public, throws so uninstall can roll back): assert(isCanonicalPluginServerKeyPrefix(keyPrefix)) (agentPlugins/mcpConfig.ts:68); return this.runExclusive(() => this.updateEnabledPluginServersLocked(list => list.filter(k => !k.startsWith(keyPrefix)))). Missing file → no-op; malformed/duplicated → throws (a lenient read would silently keep the stale key).
  3. src/node/services/agentPlugins/installService.ts
    • deps (:455-465): mcpConfigService?: Pick<MCPConfigService, "pruneEnabledPluginServers">.
    • uninstall: insert after renameIntoStaging(targetPath, trashDir) and the restoreTree definition (:2896-2915) and before the optional plugin-data staging (:2917), so the rollback is single-resource:
      // Revoke global enablement only once the tree is out of discovery, so a
      // concurrent Settings toggle validating under mcp-config.lock cannot
      // re-add it (see MCPConfigService.pruneEnabledPluginServers).
      try {
        await this.deps.mcpConfigService?.pruneEnabledPluginServers(serverKeyPrefix);
      } catch (error) {
        if (await restoreTree("failed global MCP enablement prune")) await consumeJournal();
        throw new Error(`Failed to revoke the plugin's global MCP enablement: ${getErrorMessage(error)}`);
      }
      The journal is preserved whenever restoreTree fails (existing contract → reconcileJournals restores later). The catch promises installation rollback/recovery only — consent may already be revoked and is never restored as compensation. Failures after this point (data staging, registry commit) use the existing rollback paths with the same consequence (documented in the comment).
  4. src/node/services/di/layers/desktop.ts:376-380: mcpConfigService: yield* MCPConfig (tag imported at :82).
  5. Tests
    • mcpConfigService.test.ts: switch enable-list fixtures to canonical keys (plugin:0123456789abcdef:srv with plugin: { sourceScope: "global", … }).
      • Rewrite "plugin servers are never persisted…" (:375): setServerEnabled(plugin, true) succeeds; servers on disk lacks the key; enabledPluginServers contains it; removeServer/setToolAllowlist still fail.
      • Global enable flips the plugin-layer default and persists across service recreation (new MCPConfigService over the same rootDir), including from a previously missing mcp.jsonc (resulting file has enabledPluginServers but no servers); disable removes the key and the field is omitted when empty; an ordinary addServer afterwards preserves the list.
      • Ignored inputs: a list entry naming a user server, a non-canonical key, an undiscovered key, or a sourceScope: "project" plugin server → no effect; toggling a project-scope plugin server returns Err.
      • Field-only write preserves unrelated content (a comment and an unknown top-level field survive a toggle, byte-for-byte outside the edited property); malformed file → pruneEnabledPluginServers throws and file bytes unchanged; duplicated top-level enabledPluginServers → lenient read grants nothing, toggle returns Err, prune throws, bytes unchanged; missing file → no-op; prune removes only prefix-matching keys.
      • Failure paths: writeFileAtomic rejected (spy) → toggle returns Err, file bytes unchanged; same for prune → throws. Lock-acquisition timeout (test holds <root>/mcp-config.lock with a tiny acquireTimeoutMs override) → Result Err, no throw.
      • Concurrency (deterministic, real temp files; barriers are test-controlled promises; ordering is proven with an ordered event log of transactions recorded by spies on fs.promises.readFile/writeFileAtomic for mcp.jsonc — assert transaction ordering (each operation's write lands before the next operation's first read), not literally alternating reads/writes, since a toggle may read the file more than once; never assert on "promise still pending"):
        1. Same instance: Promise.all of setServerEnabled(plugin, true), addServer("x"), removeServer("y") → final file consistent (all three effects present) and transactions do not interleave in the log.
        2. Two independent MCPConfigService instances over one rootDir (so the in-process queue cannot mask missing cross-process locking), discovery before stage-out, write delayed: A setServerEnabled(plugin, true) blocks in its in-lock provider stub on a barrier; B pruneEnabledPluginServers(prefix) is started and the test positively synchronizes on B's lock attempt (spy on acquireCrossProcessLock or on B's first mcp-config.lock fs access) before releasing A; log shows A's write before B's first read; final file lacks the key.
        3. Validation after stage-out: A's provider stub returns {} (tree gone) → Err("not found"), file bytes unchanged.
        4. Ordinary stale-read race: A addServer("x") blocks on a barrier after its in-lock config read (barrier installed via the readFile spy for mcp.jsonc, call-through after release); the test snapshots the file bytes at that moment and retains them; B pruneEnabledPluginServers(prefix) is started and its lock attempt is positively synchronized as in (2); release → log shows B's first read after A's write; final file has server x and lacks the pruned key (untouched list entries intact); the pre-barrier snapshot still contained the key (proves the stale-read window existed and was serialized away).
    • mcpServerManager.test.ts ("Workspace MCP overrides filtering" or sibling): globally-enabled plugin server + workspace disabledServers → excluded; globally-disabled + workspace enabledServers → included; no overrides → included; off-host runtime → excluded. Warm-manager lifecycle with the existing stdio stub pattern (:3627): serve → enable globally → serve (instance started, tools present) → disable globally → serve (instance retired at that refresh, tools absent).
    • installService.test.ts:
      • With a real MCPConfigService over the same temp root and a real provider (createAgentPluginsMcpProvider): install fixture → setServerEnabled on its canonical key → uninstall with zero workspacesmcp.jsonc lacks the key → reinstall → listServers() reports the server disabled: true.
      • With a mock (mcpConfigService: { pruneEnabledPluginServers }, mirroring :1309 / :2397 fixtures): prune is called with plugin:<instanceId>: after the tree is staged out (assert targetPath absent at call time) and before data staging / registry commit; rejecting prune → uninstall throws, plugin dir restored, registry entry intact, no lingering uninstall journal; rejecting prune and a failing restoreTree (target path blocked) → uninstall throws, journal retained, and the next reconcileJournals restores the tree.
    • workspaceMcpOverridesService.test.ts stays green after the findDuplicateProperty extraction (its duplicate-property cases exercise the moved code).

Gate: bun test src/node/services/mcpConfigService.test.ts src/node/services/mcpServerManager.test.ts src/node/services/agentPlugins/installService.test.ts src/node/services/agentPlugins/mcpConfig.test.ts src/node/services/workspaceMcpOverridesService.test.ts, make typecheck, make lint.

Commit 2 — UI (~−5 LoC)

  1. MCPSettingsSection.tsx:1195-1228: drop disabled={isPluginEntry} and the plugin tooltip branch (tooltip becomes "Enable server"/"Disable server"); rewrite the comment to "read-only definition (no edit/remove/allowlist); enable state persists to enabledPluginServers". Optimistic update / revert in handleToggleEnabled (:883-914) already covers backend Err.
  2. MCPSettingsSection.stories.tsx: AgentPluginServer story with a mocked global plugin: entry. Play (behavioral): switch is enabled; clicking calls the mocked mcp.setEnabled with the plugin key and the row flips; a second variant where the mock returns { success: false, error } asserts the optimistic revert and the error banner. Row layout is unchanged, so no new viewport variant; but capture a 390px screenshot in dogfooding.

Gate: make static-check; Storybook test-runner for the new stories.

Commit 3 — Docs (~+2 LoC)

docs/config/mcp-servers.mdx:53-62: "Disabled by default — globally installed plugins (~/.xum/plugins) can be enabled for all eligible local workspaces in Settings → MCP (persisted as enabledPluginServers in ~/.xum/mcp.jsonc); repo plugins are enabled per workspace via the Workspace MCP dialog; workspace overrides always win"; "Read-only — definitions cannot be edited or removed and are never written into mcp.jsonc; only their keys appear in enabledPluginServers, which a managed uninstall clears". Mention the field in the ## Configuration file example. Keep "Host-only" bullet.

Gate: make static-check (includes mintlify broken-links).

Final combined gate (after all three commits, in one run)

make static-check + the full Commit 1 test list + Storybook test-runner for the new stories, executed against the final tree (not the intermediate backend-only state), then the dogfooding pass below.

Acceptance criteria

  • Settings → MCP: a global plugin server's toggle is interactive; toggling on writes enabledPluginServers (no servers entry, other file content/comments preserved); toggling off removes the key and the field when empty; a backend error reverts the switch and shows the error.
  • Workspace MCP dialog shows a globally-enabled plugin server as enabled by default; disabling there writes disabledServers and only that workspace loses it; an existing per-workspace enabledServers entry keeps a server on when the global enable is removed.
  • Tool assembly includes the plugin server at the next workspace catalog/serve refresh after global enable and excludes it at the next refresh after disable (same semantics as ordinary global servers; no immediate termination promised); SSH/devcontainer workspaces still never get plugin servers.
  • addServer/removeServer/setToolAllowlist still reject plugin keys; repo-scope plugin servers cannot be enabled globally; a duplicated/malformed enabledPluginServers grants nothing and is rejected by mutations.
  • Managed uninstall revokes the plugin's global enablement after stage-out and before data staging/commit; prune failure aborts the uninstall with the installation rolled back (or journaled for recovery) — consent may remain revoked, never silently restored; a same-name reinstall starts default-disabled with zero or many workspaces; no previously persisted or already-validated enable survives a successful uninstall.
  • All mcp.jsonc writers (toggle, add/remove/allowlist, prune) are serialized by mcp-config.lock — verified by the deterministic two-instance tests above.
  • All listed test files, make static-check, and docs pass.

Dogfooding (evidence: desktop + 390px screenshots and a WebM; kept local unless the user later authorizes a PR/upload — this request does not include opening a PR)

  1. Isolated sandbox: KEEP_SANDBOX=1 make dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-providers --clean-projects" (skill dev-server-sandbox); note XUM_ROOT/VITE_PORT. Create a disposable git repo under $HOME/.cache/restrictions-s18v/dogfood-repo and add it as the only project. Write a sandbox-only providers.jsonc pointing anthropic at a loopback provider stub ($HOME/.cache/restrictions-s18v/provider-stub.ts, ~60 lines: accepts POST /v1/messages, appends the request's tools[].name to requests.jsonl, replies with a one-sentence SSE text response) with a dummy key — no real credentials, no egress.
  2. Owned MCP fixture (no unpinned bunx -y): copy tests/fixtures/agent-plugins/hello-plugin to <XUM_ROOT>/plugins/hello-plugin and point its mcp.json server at a ~40-line stdio JSON-RPC script in $HOME/.cache/restrictions-s18v/echo-mcp.ts (handles initialize, tools/list with one echo tool, tools/call), launched with the pinned bun from mise.
  3. agent-browser open http://localhost:<VITE_PORT>; agent-browser record start $HOME/.cache/restrictions-s18v/dogfood.webm; Settings → Experiments → enable Agent Plugins; Settings → MCP → snapshot -i: hello-plugin/<server> row with an enabled Switch (screenshot 01-toggle-enabled.png, plus an agent-browser viewport 390 844 variant); click → 02-after-enable.png; cat <XUM_ROOT>/mcp.jsonc shows enabledPluginServers and no plugin: key under servers.
  4. Create a workspace → Workspace MCP dialog shows the server enabled by default (03-workspace-default-on.png); disable it → <worktree>/.xum/mcp.local.jsonc has disabledServers; XUM_ROOT=<sandbox> bun run debug plugins <workspace-id> prints the plugin layer with disabled: false and the override.
  5. Runtime through the real assembly path (turnRequestBuildermcpServerManager → provider request): in a second workspace (no override) send any prompt; the loopback stub's requests.jsonl must list the plugin's echo tool in the assembled request (04-tools-assembled.png of the transcript + the stub log excerpt). Toggle off globally → send another prompt → the next request lacks it (05-tools-gone.png); toggle on again and disable per-workspace via the Workspace MCP dialog → request lacks it in that workspace only. (Optional, only if the user wants it: repeat one turn with a real provider — never print keys.)
  6. Uninstall path: Settings → Plugins → install the fixture via the managed installer (local path source), enable its server globally, uninstall → mcp.jsonc no longer lists its key; reinstall → server shows disabled (06-reinstall-default-disabled.png). record stop; attach PNGs/WebM in chat with attach_file.
  7. Cleanup: stop the provider stub and any echo-mcp.ts processes by PID (never pkill -f from the same script), stop the sandbox dev server task, remove the sandbox XUM_ROOT and the disposable repo; keep only the evidence files under $HOME/.cache/restrictions-s18v/.

Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $282.81

Serialize MCP configuration mutations and revoke global enablement during managed uninstall without persisting plugin definitions. Preserve workspace override precedence.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$50.89`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=50.89 -->
Keep plugin definitions read-only while enabling their global toggle. Cover successful toggles and optimistic rollback on backend errors.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$50.89`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=50.89 -->
Document the key-only consent list, workspace override precedence, and managed uninstall behavior. Sync the bundled documentation snapshot.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$50.89`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=50.89 -->
@mintlify

mintlify Bot commented Sep 13, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Mux 🟢 Ready View Preview Sep 13, 2026, 10:08 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@chatgpt-codex-connector

This comment has been minimized.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee2bd3f8cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/agentPlugins/installService.ts
@ThomasK33

ThomasK33 commented Sep 13, 2026

Copy link
Copy Markdown
Member Author

Recorded UAT passed on the rebased commit

Verified ee2bd3f8cd76d591cfe3644892c948eb5897018f through the real application UI, with an isolated local MCP fixture and loopback Anthropic provider. No real provider credentials or external model traffic.

  1. Global enablement: on persists only the canonical key; removing the last key leaves {}. No plugin definition is copied into servers.
  2. Responsive controls: desktop and native 390×844 controls work; the mobile document has no horizontal overflow.
  3. Workspace precedence: A's disable is isolated from B. Explicit workspace enablement also wins when the global default is off.
  4. Actual request assembly: six main-chat requests verified the serialized MCP tool list:
Request state Echo tool
A globally on Present
B globally on, A disabled Present
B globally off Absent
A globally on, workspace disabled Absent
B globally re-enabled Present
A globally off, workspace enabled Present
  1. Managed lifecycle: install and reinstall used component-selection consent (echo selected, greeter deselected). Global enablement was pruned on uninstall; reinstall remained disabled.

All five recordings decode cleanly; sampled and final frames were checked. Owned browser, provider, MCP, and sandbox processes were stopped, and disposable fixtures were removed. Off-host behavior is covered by automated tests, not live SSH/devcontainer UAT.

Screenshots

Globally enabled plugin server on desktop

Global plugin toggle at 390px

Managed reinstall starts disabled

Recordings

Desktop global toggle — 35.4s

02-global-desktop.webm

Native 390px toggle — 49.1s

03-global-mobile.webm

Workspace override isolation — 93.5s

04-workspaces.webm

Main-chat tool presence matrix — 148.4s

05-prompt-policy.webm

Managed consent, uninstall, and reinstall — 179.7s

06-managed-lifecycle.webm

Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $91.02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛡️ Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: ee2bd3f8cd

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/node/services/mcpConfigService.ts
Clear prior-path enablement before a fresh managed install and abort promotion
when consent cleanup fails. Strip global plugin enablement during backup restore
so a backup cannot grant or resurrect machine-local execution consent.

Cover manual removal/reinstall, malformed cleanup, backup injection and approval,
redacted consent, and duplicate fields with real-service regression tests.

---

_Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$121.33`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=121.33 -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b19f8dacfc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/mcpConfigService.ts
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: b19f8dacfc

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

Copy link
Copy Markdown
Member Author

Readiness: blocked — not ready to merge

  1. Current commit: b19f8dacfc50cf3b671d050a6e7035a12a45b26b. The fresh-install and backup-restore consent findings are fixed, tested, and resolved.
  2. Validation: make static-check-full, 908 backend tests, and 15 MCP Settings Storybook tests passed. All build/test jobs in CI run 34785875389 passed. Security review is clean on this commit. Codex Comments and Required remain failed; the PR is mergeable but not ready.
  3. Remaining blocker: cross-backend consent revocation. A global plugin disable advances only the writing backend's in-memory generation; another backend's held-tool gate does not read that global consent change. This affects the new consent surface, so the finding remains unresolved rather than being deferred as an existing ordinary-MCP limitation.
  4. Stopping decision: six combined review rounds are complete. The independent recommendation was considered; its proposed deferrals were not adopted, and both original findings were fixed. The final strategic advisor recommendation and worker decision are blocked. I am pausing at the review limit, not claiming success or starting a seventh round. No merge was performed.

Next step, after authorization to continue beyond the review limit: add a deterministic shared-home, two-backend held-tool regression; then fence/re-read global consent at dispatch using the existing MCP config lock, failing closed when consent cannot be established. Prefer this bounded change over a new persisted epoch subsystem. Re-run the affected gates and obtain a clean review. This is a proposed fix, not implemented or verified yet.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $121.33

Read global execution consent under the existing MCP config writer lock at
held-tool and prompt admission. Preserve explicit workspace enablement and
release admission locks before waiting for the tool result.

Cover sibling revocation and re-enablement, malformed consent, writer contention,
cancellation, late acquisition cleanup, and both prompt admission paths with
real config-service regressions. Complete existing partial prompt fixtures.

---

_Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$160.30`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=160.30 -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@ThomasK33

ThomasK33 commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

Recorded cross-backend revocation check — passed

Tested commit: a0469f0068ef3296b6e34030a307c602e55851e5.

Two separate Bun backends shared a disposable Xum home. A used the real config writer; B used the real config service, manager, plugin discovery, and stdio MCP transport. B kept the same two issued tool objects throughout; neither was re-served.

Step Actual cumulative MCP calls
Invoke held globally enabled tool 1
Disable globally in A; invoke the same held tool in B 1 — rejected without dispatch
Invoke B's explicitly enabled workspace tool 2
Re-enable globally; invoke the original held tool 3

Baseline and recovery used the same MCP process. All backend, MCP, terminal, browser, and recording processes were stopped; owned ports closed and disposable homes removed.

Scope: service/process/stdio boundary using an unmanaged global fixture plugin. This is not a new Settings UI, AI request, managed-plugin lifecycle, or off-host walkthrough. The earlier desktop/mobile UI evidence remains linked in the PR description.

Rejection at the held-tool boundary

The sibling backend rejects the previously issued tool without another MCP call

Complete walkthrough

Revocation, explicit workspace consent, and recovery all pass using the retained tool objects

Recording

Real-time terminal walkthrough, 193 seconds at 1600×1200. No replay or time compression. A ttyd-only compatibility shim restored standard WebSocket state constants in the owned browser context; network allowlisting stayed enabled.

cross-backend-revocation-final.webm

Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $170.48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0469f0068

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/mcpServerManager.ts
Comment thread src/node/services/mcpConfigService.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛡️ Codex Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: a0469f0068

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/node/services/mcpConfigService.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

Readiness after round 7: blocked, not ready to merge

The held-tool revocation fix is implemented and verified on a0469f0068ef3296b6e34030a307c602e55851e5: full static checks, 921 backend tests, and recorded real-process MCP UAT passed. All executed static/build/test jobs in CI run 34821307846 passed. The review gate and Required check remain failed.

The new code/security reviews have three unresolved threads representing two distinct issues:

  1. Startup is still unfenced against global revocation. Code finding and security finding describe the same gap. I missed this sibling admission path: launchUnderOverrideFence checks workspace/component state, not global consent, so a cold plugin can still start after a sibling disable. The held-tool fix does not establish launch safety. Both threads remain unresolved.
  2. Empty/comments-only configuration blocks toggles and pruning. Finding. The strict parser rejects a rootless document, so harmless empty configuration can block required consent cleanup. This also remains unresolved.

Decision: blocked. Seven combined review rounds have now been used, including the explicitly announced additional cycle. The authorization work is not converging yet, so I am pausing rather than silently starting round 8 or deferring these defects. No merge was performed. The strategic advisor returned no usable output on two attempts; this decision follows the verified code, review, and CI evidence.

Proposed next increment: reuse the existing consent fence through actual process/connection initiation, preserving explicit workspace consent; normalize valid empty/comment-only JSONC without accepting malformed documents. Add deterministic startup-race and empty-config regressions before implementation, then rerun the gates and review. Both fixes belong to this feature; no new epoch subsystem or unrelated PR is proposed. They are not implemented or verified yet. Further work/review awaits direction beyond the announced cycle limit.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $181.70

Reuse the existing global/component consent decision through actual process and
connection initiation, including automatic transport fallback. Preserve explicit
workspace consent and the current bounded startup lock lifetime.

Treat valid empty and comment-only MCP JSONC as no consent while still rejecting
malformed or non-object content. Preserve comments during toggles and pruning.

Add stale-snapshot startup coverage across all transports and override paths,
real writer-lock probing at exec, fallback revocation, and empty-config regressions.

---

_Generated with `xum` • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$225.83`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=225.83 -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e4c33acb4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/backup/payload.ts
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 1e4c33acb4

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

ThomasK33 commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

Recorded startup and empty-config checks — passed

Tested commit: 1e4c33acb4d1056c89938f3f8aac766c5d78d626.

Two separate Bun backends shared a disposable home. An injected orchestration barrier paused B after the real MCP config service returned its enabled snapshot, before returning that snapshot to the real manager. A then completed global disable; only afterward did B resume. Plugin discovery, the MCP client, and stdio transport were real.

Step Process starts Initializations Tool calls
Release B's stale snapshot after A completed disable 0 0 0
Start with explicit workspace consent while globally disabled 1 1 1
Re-enable globally and start normally 2 2 2

At the rejected startup boundary there was no live MCP process. Comments-only enable, disable, and prune also succeeded while preserving both comments and the correct effective consent.

Stale startup blocked before execution

Stale startup is rejected with zero process starts, initialization requests, or tool calls

Full result and positive controls

Comments are preserved, stale startup is blocked, and workspace consent plus re-enablement succeed

Recording

Real-time terminal actions, approximately 192 seconds, with no replay or time compression. The recorder scaled the view to 1600×722; the PNGs retain 1600×1200. The final video frame retains all terminal content.

cold-start-config.webm

Scope: service/process/stdio boundary using an unmanaged global fixture plugin, with the barrier disclosed above. This is not a full Settings UI, AI request, managed-plugin lifecycle, or off-host walkthrough. All owned processes were stopped, three ports closed, and disposable homes removed. Earlier UAT artifacts are unchanged.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $241.50

@ThomasK33

Copy link
Copy Markdown
Member Author

Readiness after round 8: blocked, not ready to merge

Final pushed commit: 1e4c33acb4d1056c89938f3f8aac766c5d78d626. The previous startup and empty-JSONC defects are fixed, and their three review threads are resolved.

  1. Validation passed: 945 backend tests across eight files, full local static checks, and recorded real-process startup/config UAT. All executed CI static/build/test suites passed in run 34834364833. The Codex Comments and Required checks remain failed; overall CI is not green.
  2. Review: round 8 has settled, including the post-review quiet window. Security review is clean on this commit. Code review found one remaining backup-restore consent-ordering defect: restore clears plugin consent without the shared MCP writer lock, allowing an already-fenced operation to dispatch/start after restore returns. I confirmed that the restore write path does not take the lock. This PR introduced the consent behavior, so this is not an unrelated cleanup item. The thread remains unresolved.
  3. Decision: paused at the agreed review extension, rather than silently starting round 9. The readiness advisor recommends preserving this pushed candidate and obtaining a bounded extension before another fix/review cycle. No merge or additional code changes have been made.

The proposed correction stays in this PR: reuse the existing lock around the MCP restore read/merge/write interval, preserving existing backup validation and filesystem protections. Add a deterministic regression proving that a held real consent fence blocks restore, release permits restore to clear consent, and subsequent admission rejects; also prove lock release on failure. No new consent model, persisted state, or unrelated backup refactor is proposed. Then run validation and one further agreed review cycle. The blocker remains open here, not deferred as fixed.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $264.47

Reuse the existing MCP writer lock for restore approval, local merge and
persistence so clearing plugin consent is ordered with held invocations and
startup. Preserve the complete backup preflight before settings writes.

Regressions cover a held real consent fence, a fresh local-only merge,
post-restore admission denial, and release after approval/write failures.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh` • Cost: `$275.67`_

<!-- mux-attribution: model=coder:openai/gpt-6-astra thinking=xhigh costs=275.67 -->
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@ThomasK33

ThomasK33 commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

Recorded backup-restore consent check — passed

Tested commit: 21118f90898ab939e4390ff6e34d8d042fe6841c.

One isolated Bun process ran real MCP configuration/admission and backup restore operations concurrently against a disposable home. Filesystem observation detected the actual lock-publication attempt; neither the lock nor restore implementation was mocked.

  1. Held the real admission fence.
  2. Started restore. It remained queued, with consent bytes unchanged.
  3. Released the fence. Restore completed with consent removed, and the next admission rejected as globally disabled.
  4. Fresh explicit enablement allowed admission again.

This is service/filesystem validation, not full Settings UI, AI, or off-host testing. The regression suite separately proves that restore merges fresh local-only settings after lock acquisition and releases the lock on approval/write failures. All owned processes stopped, both ports closed, and disposable homes were removed.

Final assertions

All four restore-ordering stages passed on the final commit

Recording

221.5 seconds, 1600×900. The recording includes the held-fence, queued-restore, denial, and fresh-enable stages.

restore-consent-ordering.webm

Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $282.81

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 21118f9089

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 21118f9089

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

Copy link
Copy Markdown
Member Author

CI review-check bookkeeping: resolved findings, stale summary marker

On unchanged head 21118f90898ab939e4390ff6e34d8d042fe6841c, all real test/build checks now pass, including the integration retry. Code and security reviews are clean, and GitHub reports all seven review threads resolved.

The remaining Codex Comments failure is summary comment 5656252910. Its historical startup-fence advisory still lacks a Resolved marker, although the underlying finding is fixed and its GitHub thread is resolved:

  1. Restored plugin consent: finding, fix response.
  2. Startup revocation fence: finding, fix response, recorded startup verification.
  3. Fresh current-head approvals: code, security.

I am marking only that stale summary comment Resolved using GitHub's comment-resolution mechanism, which the existing checker supports. The actual findings, replies, and clean verdicts remain intact. No bot text, source, workflow, or branch-protection rule is being changed. The unchanged checker and required CI must still pass before merge. I will inspect the raw summary again before merge; any new finding or change to the reviewed head/review state invalidates this resolution and requires reassessment.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $292.90

@ThomasK33
ThomasK33 added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 14cf7fb Sep 14, 2026
51 of 57 checks passed
@ThomasK33
ThomasK33 deleted the global-plugin-mcp-enablement branch September 14, 2026 14:28
@ThomasK33

Copy link
Copy Markdown
Member Author

Merged — final validation record

Merged through the required queue at 2026-09-14 14:28:22 UTC as 14cf7fbdd22b2c14cf0199232da178cf4eccbdfc. Reviewed candidate: 21118f90898ab939e4390ff6e34d8d042fe6841c.

  1. Validation: 947 backend tests and full local static checks passed. Recorded restore UAT passed; screenshot/video uploads were byte-verified and all owned test processes cleaned up.
  2. Reviews: code and security clean on the final candidate; all seven threads resolved. Finished in round 9—the first of the two additional authorized cycles. The independent readiness recommendation was ready subject to the remaining gates, which subsequently passed.
  3. CI: branch CI and merge-group CI succeeded. The stale summary bookkeeping is documented here; its raw body remained unchanged through merge.

Stopping because the reviewed candidate passed all required gates and GitHub confirmed the merge. No additional implementation or review cycle was needed.


Generated with xum • Model: coder:openai/gpt-6-astra • Thinking: xhigh • Cost: $292.90

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