Home

mweinbach / agent-coworker

publicmweinbach/agent-coworker
Overview Code History Branches Pull requestsIssuesInsights
main
HomeOverview Code PRsIssues

feat: add plugin install management flow (#67)

6 months ago

2dd6419
Authored
Max Weinbach4/10/2026, 9:35:12 PM
* feat: add plugin runtime foundation

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add desktop plugin management

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix: polish plugin desktop walkthrough

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix: narrow plugin MCP transport typing

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* test: account for plugin bootstrap in jsonrpc desktop tests

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* feat: add plugin install management flow

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix(desktop): surface plugin preview JSON-RPC errors and ensure control readiness

- Add optional errorDetailOut to requestJsonRpcControlEvent (catch, disposed,
  and inline error events) so callers can show server messages
- previewPluginInstall: await ensureServerRunning, ensureControlSocket, pass
  error sink into RPC, use trimmed message in clearFailedPluginMutationSend

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix: reset plugin install dialog state

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* test: fix CI plugin management expectations

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix: scope plugin mcp auth by plugin layer

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* fix ui

* Track plugin scope in desktop plugin selection state

* fix: harden plugin and skills management regressions

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* test: finalize plugin regression coverage

Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>

* Fix plugin precedence and global skill catalog visibility

* Update .gitignore

* fix-bugs

* fix: harden plugin install validation and control updates

* Update SkillsCatalogPage.tsx

* fix(test): correct plugin disable test expectations

Use `name` instead of `skillName` for installation lookups, and update
disable assertions to expect disabled skills to remain visible with
`enabled: false` rather than being removed entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent invalid plugin install sources from masquerading as valid bundles

Skill-only plugin bundles were being treated like malformed MCP/app
configs, explicit skills path typos were silently ignored, and local
manifest-file installs resolved to the wrong directory. Tighten the
manifest/source boundary so valid bundles stay quiet and invalid inputs
fail early with useful diagnostics.

Constraint: Plugin manifests may omit mcpServers and apps while still remaining valid
Rejected: Keep implicit .mcp.json/.app.json defaults unconditionally | still emits bogus MCP diagnostics for skill-only bundles
Rejected: Let missing explicit skills paths degrade to empty directories | hides manifest typos and installs broken bundles silently
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Only infer optional plugin asset paths when the backing file exists, and treat explicit manifest paths as contracts
Tested: bun test test/plugins.catalog.test.ts test/plugins.sourceResolver.test.ts; bun run typecheck; bun test
Not-tested: Manual desktop plugin install walkthrough

* fix: prevent broken plugin management targets and bundled MCP startup

The new plugin-management flow persisted a workspace-specific target
but did not restore or sanitize it consistently, which left the UI
pointing at deleted workspaces after restart or removal. Plugin-bundled
stdio MCP servers also lost their plugin-root context, so relative
command and cwd values resolved against the process cwd instead of the
plugin install location.

Constraint: Desktop plugin management must tolerate deleted workspaces and warm-cache restore
Constraint: Plugin-bundled MCP servers may declare relative stdio command and cwd paths
Rejected: Patch each view/action independently | would duplicate stale-workspace checks across desktop surfaces
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep plugin management workspace resolution centralized so restore, runtime actions, and UI fallback stay aligned
Tested: bun test test/mcp.config-registry.test.ts apps/desktop/test/bootstrap-cache.test.ts apps/desktop/test/skills-actions.test.ts apps/desktop/test/workspace-settings-sync.test.ts
Tested: bun run typecheck
Tested: bun test
Not-tested: Manual packaged desktop interaction against an installed app build

* fix: harden scoped plugin skill discovery

* fix: close plugin discovery and MCP regressions

Review feedback uncovered two classes of issues: plugin-bundled MCP configs could
escape their bundle root or fail when launched through interpreters, and the
desktop plugin view conflated "unset" with "explicit global" selection.

This hardens plugin stdio rebasing for command/cwd/path-like args, allows
direct discovery of symlinked plugin directories, and introduces an explicit
plugin-management mode so the selected workspace is the default catalog target
without removing intentional Global selection.

Constraint: Preserve the existing Global selector semantics while fixing the first-open workspace default
Rejected: Derive workspace scope directly from catalogWorkspaceId | would make explicit Global filtering impossible once a workspace is selected
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep pluginManagementMode and pluginManagementWorkspaceId in sync when changing desktop plugin-management state semantics
Tested: bun test test/mcp.config-registry.test.ts
Tested: bun test test/plugins.catalog.test.ts
Tested: bun test apps/desktop/test/plugin-management.test.ts
Tested: bun test apps/desktop/test/settings-nav.test.ts apps/desktop/test/bootstrap-cache.test.ts apps/desktop/test/app-topbar.test.tsx apps/desktop/test/skills-catalog-page.test.ts apps/desktop/test/plugins-catalog-page.test.ts
Tested: bun run typecheck
Tested: bun test
Not-tested: none

* fix: preserve valid plugin bundle inputs during install and MCP loading

Bundled plugin MCP stdio args can legitimately contain slashes without being
filesystem paths, and GitHub tree URLs copied from the UI can point directly
at .codex-plugin.

This narrows path rebasing to explicit or existing plugin-relative filesystem
paths while preserving package names and URLs, and it normalizes tree-based
GitHub installs back to the plugin bundle root before materialization.

Constraint: Plugin-bundled stdio args may be package names or URLs, not only file paths
Constraint: GitHub UI tree links may target .codex-plugin instead of the bundle root
Rejected: Rebase every slash-containing stdio arg | breaks scoped packages and URL arguments
Rejected: Skip preferred tree materialization attempts | normalizing tree subdirs is more direct and keeps descriptors accurate
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep plugin-local rebasing limited to genuine filesystem paths and normalize .codex-plugin tree URLs before download
Tested: bun test test/mcp.config-registry.test.ts; bun test test/plugins.sourceResolver.test.ts; bun run typecheck; bun test
Not-tested: none

* fix: prevent silent plugin installs and stale shared plugin state

Plugin install preview only validated bundled skills, so malformed bundled MCP configs could preview as installable and then fail later at runtime. This change validates bundled .mcp.json files during preview/install, and it broadens shared-scope mutation refresh fan-out so user-scoped plugin changes refresh other live sessions instead of only the active workspace.

Constraint: Shared plugin state is user-scoped while live sessions are keyed by workspace directory

Rejected: Keep refresh fan-out workspace-bound and rely on reconnects | leaves live sessions stale after shared plugin mutations

Rejected: Treat bundled MCP parsing as a runtime-only warning | allows preview/install to claim success for broken plugins

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Shared-scope plugin and skill mutations must keep cross-workspace refresh semantics aligned with install scope

Tested: bun test test/plugins.catalog.test.ts

Tested: bun test test/session.test.ts --test-name-pattern "plugins"

Tested: bun run typecheck

Not-tested: standalone verification of legacy websocket delivery across multiple live workspace sessions

* fix: keep standalone user skills visible in the global catalog

The desktop Global skills filter was excluding existing standalone user-scope skills unless they belonged to a plugin. This restores the expected user-wide catalog view by including all user-scope installations and adds a regression test alongside the existing plugin-owned user-skill case.

Constraint: The desktop Global view must preserve the existing skill catalog model while separating project-only content

Rejected: Special-case plugin-owned user skills only | hides valid standalone user installs under ~/.agent/skills

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Global skills filtering should exclude only project-local entries, not standalone user-scope installations

Tested: bun test apps/desktop/test/skills-catalog-page.test.ts

Tested: bun test

Not-tested: manual desktop renderer interaction outside jsdom

* fix: preserve plugin installs across ambiguous refs and MCP server renames

GitHub tree/blob/raw plugin installs were accepting the initially parsed ref first, which breaks URLs whose refs contain slashes. Resolve ambiguous ref/path splits by preferring the longest exact ref that exists, and only finalize an attempt after candidate discovery runs on a clean staging tree.\n\nPlugin replacement installs also need to carry bundled MCP credentials forward when the bundled server name changes. Infer safe one-to-one renames from matching server definitions and move credentials within the target plugin scope after the new bundle is copied.\n\nConstraint: GitHub tree/blob/raw URLs with slash-delimited refs must resolve the same ref GitHub would select\nConstraint: Bundled MCP credentials are keyed by server name within the plugin auth scope\nRejected: Continue trying shorter ref/path splits after a longer exact ref exists | can resolve the wrong source tree\nRejected: Blindly rename all removed and added bundled MCP servers | ambiguous multi-server updates could misapply credentials\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Keep ambiguous GitHub source resolution aligned with exact-ref selection before falling back to shorter splits\nTested: bun test\nTested: bun run typecheck\nTested: bun test test/plugins.sourceResolver.test.ts test/plugins.catalog.test.ts test/mcp.auth-store.test.ts\nNot-tested: Live GitHub installs against private repositories with slash-delimited refs

* fix: keep shared plugin and skill state coherent after installs

Same-scope plugin reinstalls only removed the canonical target directory, which left marketplace or manually-linked copies behind and made same-scope plugin resolution ambiguous. This change removes every existing install root for the same plugin id within the target scope before copying the new bundle.\n\nShared user/global mutations also need two refresh lanes. Live server sessions now refresh across workspace-control bindings and coordinate cross-process reloads through a shared mutation signal file, while the desktop store proactively refreshes catalog and MCP state for other already-running workspaces after shared mutations complete.\n\nConstraint: Desktop runs one workspace server process per open workspace, so shared skill and plugin mutations cannot rely on in-process session maps alone\nConstraint: Same-scope plugins may be discovered from marketplace entries or other non-canonical roots, not just <pluginsDir>/<pluginId>\nRejected: Rely only on control-session notifications for shared refreshes | changed existing JSON-RPC response ordering and regressed server.jsonrpc websocket tests\nConfidence: high\nScope-risk: moderate\nReversibility: clean\nDirective: Shared user/global mutations must keep server prompt refresh and desktop catalog refresh in sync; do not assume one path covers both\nTested: bun test test/plugins.catalog.test.ts test/server.test.ts apps/desktop/test/skills-actions.test.ts\nTested: bun run typecheck\nTested: bun test\nNot-tested: none

* fixes

* fixes

* fixes

* fix: keep plugin control state fresh across shared mutations

Shared plugin and skill mutations now rebroadcast refreshed control-plane state to subscribed desktop JSON-RPC clients, so passive plugin/skills views no longer go stale until a manual refresh. The desktop plugin actions also preserve concrete server-side validation errors instead of flattening them into generic connectivity failures, and the install dialog now treats preview scope as part of the preview contract.

Constraint: Shared mutation refresh needed to work over the existing JSON-RPC control socket instead of adding a new transport
Rejected: Rely on ephemeral workspace-control bindings for passive refresh | they cannot notify an idle desktop client
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep plugin mutation failures routed through plugin-specific runtime fields so the plugin detail surfaces show the real server error
Tested: bun run docs:generate-jsonrpc; bun test test/jsonrpc.codegen.test.ts; bun run typecheck; env -u REMODEX_DEVICE_STATE_DIR bun test
Not-tested: Cross-process refresh behavior outside the automated local multi-client test harness

* Create config.toml

* fix: respect workspace cwd and CRLF prompt overlays

* Fix control event routing and plugin MCP catalog warnings

* Fix plugin install MCP migration and timeout handling

* test: keep slow plugin install coverage from failing the control lane

The plugin install control test intentionally delays installPlugins past five seconds to exercise the JSON-RPC mutation-event window. Bun's default 5s timeout was killing that test before cleanup ran, which leaked the prototype monkeypatch into the next plugin install assertion.

The checked-in JSON-RPC artifacts were also stale, so the schema docs are regenerated to match the current plugin MCP metadata shape exposed by the control contract.

Constraint: Slow-path control coverage intentionally exceeds Bun's default 5s timeout
Rejected: Shorten the mocked install delay | would stop covering the slow mutation-event path this test exists to guard
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any test that intentionally waits past 5s must set an explicit Bun timeout so cleanup still runs on failure paths
Tested: bun test ./test/jsonrpc.codegen.test.ts ./test/server.jsonrpc.control.test.ts
Tested: bun test
Tested: bun run typecheck
Tested: bun run docs:check
Not-tested: GitHub Actions rerun for PR #67

* test: stop deferred skill refresh assertions from racing the event loop

The deferred external skill refresh is launched after turn completion as follow-up async work, so one flush tick is not a reliable signal that the emitted skills_list event has already landed. The session test now waits for the actual event instead of assuming scheduler timing.

Constraint: The runtime intentionally fires the deferred skill refresh after the turn settles instead of blocking turn completion on refresh I/O
Rejected: Await the deferred refresh inside sendUserMessage completion | would change the runtime completion contract to satisfy a test-only timing assumption
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Tests that assert on post-turn background refresh events should wait on the event they need, not a single flush tick
Tested: bun test ./test/session.test.ts -t "defers external skill refresh until the active turn completes"
Tested: bun test ./test/jsonrpc.codegen.test.ts ./test/server.jsonrpc.control.test.ts
Tested: bun test
Tested: bun run typecheck
Not-tested: GitHub Actions rerun for PR #67 after follow-up push

* Fix plugin install rollback and nested bundle discovery

* Fix symlinked plugin bundle and skill discovery

* Fix plugin install validation and preserve plugin detail errors

* codex auth rework

* Prevent control session persistence and harden plugin rollback tests

* fix: address plugin install review feedback

Keep ambiguous GitHub plugin sources installable, restore the desktop uninstall and waiter guards, and stop tracking local agent config in the repo.

* fix: harden control lifecycle registration

Clean up partial control-listener state before re-registering and clarify that plugin previews are scope-specific so the install dialog guidance matches the enforced behavior.

* fix: address remaining PR review feedback (#67)

- Fix inverted preview logic: use === true instead of !== false for
  fromUserPreviewRequest in plugin/skill install preview handlers
- Add cwd guard in control notification handler to prevent cross-workspace
  state corruption
- Reset searchQuery when switching between Plugins and Skills tabs
- Remove unused setSearchQuery prop from PluginsCatalogPage and
  SkillsCatalogPage
- Delete dead HeaderAndFilters component and its test
- Remove old workspace subscriptions on connection reuse in
  registerWorkspaceControlSubscriber

* Update startServer.ts

* fix: address remaining plugin review feedback

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

* fix: address follow-up review comments

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Max Weinbach <mweinbach@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

Parent952c26c

112 files changed
  • .gitignore+4−0
  • CHANGELOG.md+7−1
  • apps/desktop/src/App.tsx+32−2
  • apps/desktop/src/app/pluginManagement.ts+53−0
  • apps/desktop/src/app/store.actions/bootstrap.ts+41−10
  • apps/desktop/src/app/store.actions/skills.ts+461−26
  • apps/desktop/src/app/store.actions/workspace.ts+9−1
  • apps/desktop/src/app/store.helpers.ts+11
−0
  • apps/desktop/src/app/store.helpers/controlSocket.ts+212−6
  • apps/desktop/src/app/store.helpers/persistence.ts+3−0
  • apps/desktop/src/app/store.helpers/runtimeState.ts+11−0
  • apps/desktop/src/app/store.ts+2−0
  • apps/desktop/src/app/types.ts+18−0
  • apps/desktop/src/components/ui/dialog.tsx+1−1
  • apps/desktop/src/components/ui/select.tsx+10−3
  • apps/desktop/src/lib/wsProtocol.ts+3−0
  • apps/desktop/src/ui/Sidebar.tsx+25−9
  • apps/desktop/src/ui/SkillsView.tsx+66−3
  • apps/desktop/src/ui/layout/AppTopBar.tsx+149−99
  • apps/desktop/src/ui/plugins/InstallPluginDialog.tsx+308−0
  • apps/desktop/src/ui/plugins/PluginCardGrid.tsx+67−0
  • apps/desktop/src/ui/plugins/PluginDetailDialog.tsx+233−0
  • apps/desktop/src/ui/plugins/PluginsCatalogPage.tsx+119−0
  • apps/desktop/src/ui/settings/pages/mcpServerDraft.ts+1−0
  • apps/desktop/src/ui/skills/HeaderAndFilters.tsx+0−100
  • apps/desktop/src/ui/skills/InstallationCardGrid.tsx+9−0
  • apps/desktop/src/ui/skills/SkillDetailDialog.tsx+11−4
  • apps/desktop/src/ui/skills/SkillsCatalogPage.tsx+50−14
  • apps/desktop/test/app-jsonrpc-disposal.test.tsx+1−0
  • apps/desktop/test/app-topbar.test.tsx+78−0
  • apps/desktop/test/bootstrap-cache.test.ts+121−0
  • apps/desktop/test/control-socket.test.ts+479−1
  • apps/desktop/test/header-and-filters.test.tsx+0−109
  • apps/desktop/test/jsdomHarness.ts+16−0
  • apps/desktop/test/jsonrpc-single-connection.test.ts+4−0
  • apps/desktop/test/plugin-detail-dialog.test.tsx+262−0
  • apps/desktop/test/plugin-management.test.ts+48−0
  • apps/desktop/test/plugins-catalog-page.test.ts+636−0
  • apps/desktop/test/sidebar-ui.test.tsx+98−28
  • apps/desktop/test/skill-detail-dialog.test.ts+5−0
  • apps/desktop/test/skills-actions.test.ts+605−1
  • apps/desktop/test/skills-catalog-page.test.ts+200−2
  • apps/desktop/test/workspace-mcp-editor.test.ts+89−50
  • apps/desktop/test/workspace-settings-sync.test.ts+38−0
  • docs/generated/websocket-jsonrpc.d.ts+33−20
  • docs/generated/websocket-jsonrpc.schema.json+1−1
  • docs/websocket-protocol.md+10−0
  • src/config.ts+69−0
  • src/mcp/authStore/editor.ts+5−5
  • src/mcp/authStore/resolver.ts+3−3
  • src/mcp/authStore/store.ts+31−8
  • src/mcp/configRegistry/layers.ts+161−7
  • src/mcp/configRegistry/types.ts+10−2
  • src/plugins/catalog.ts+174−0
  • src/plugins/discovery.ts+196−0
  • src/plugins/index.ts+51−0
  • src/plugins/manifest.ts+609−0
  • src/plugins/marketplace.ts+110−0
  • src/plugins/mcp.ts+176−0
  • src/plugins/operations.ts+386−0
  • src/plugins/overrides.ts+197−0
  • src/plugins/skillBridge.ts+73−0
  • src/plugins/sourceResolver.ts+644−0
  • src/prompt.ts+35−4
  • src/providers/codex-auth.ts+2−101
  • src/server/commands.ts+2−2
  • src/server/jsonrpc/routes/index.ts+2−0
  • src/server/jsonrpc/routes/outcomes.ts+32−0
  • src/server/jsonrpc/routes/plugins.ts+154−0
  • src/server/jsonrpc/routes/session.ts+53−30
  • src/server/jsonrpc/routes/types.ts+9−2
  • src/server/jsonrpc/schema.misc.ts+15−0
  • src/server/jsonrpc/schema.plugins.ts+22−0
  • src/server/jsonrpc/schema.ts+3−0
  • src/server/jsonrpc/sessionEventCapture.ts+72−0
  • src/server/protocol.ts+29−1
  • src/server/session/AgentSession.ts+66−3
  • src/server/session/PersistenceManager.ts+4−0
  • src/server/session/SessionContext.ts+8−2
  • src/server/session/SkillManager.ts+214−8
  • src/server/session/TurnExecutionManager.ts+4−0
  • src/server/sharedSkillMutationSignal.ts+50−0
  • src/server/skillMutationRefresh.ts+60−0
  • src/server/startServer.ts+342−56
  • src/shared/jsonrpcControlSchemas.ts+207−0
  • src/skills/catalog.ts+116−7
  • src/skills/index.ts+71−2
  • src/skills/operations.ts+54−1
  • src/tools/skill.ts+2−2
  • src/types.ts+143−9
  • src/utils/paths.ts+36−0
  • src/utils/permissions.ts+32−2
  • tasks/lessons.md+5−1
  • tasks/todo.md+68−0
  • test/connect.test.ts+7−7
  • test/jsonrpc.control-schemas.test.ts+12−0
  • test/jsonrpc.router.test.ts+6−3
  • test/jsonrpc.routes.review-fixes.test.ts+2−1
  • test/mcp.auth-store.test.ts+115−0
  • test/mcp.config-registry.test.ts+288−0
  • test/permissions.test.ts+24−0
  • test/plugins.catalog.test.ts+1187−0
  • test/plugins.sourceResolver.test.ts+423−0
  • test/prompt.test.ts+29−0
  • test/providerStatus.test.ts+6−11
  • test/providers/codex-auth.test.ts+23−17
  • test/providers/saved-keys.test.ts+5−8
  • test/runtime.pi-runtime.test.ts+6−12
  • test/server.jsonrpc.control.test.ts+651−2
  • test/server.test.ts+39−0
  • test/session.test.ts+146−0
  • test/skills.catalog.test.ts+86−1