Skip to content

Add local-first plugin marketplace#653

Open
GautamBytes wants to merge 8 commits into
Gitlawb:mainfrom
GautamBytes:feat/plugin-marketplace
Open

Add local-first plugin marketplace#653
GautamBytes wants to merge 8 commits into
Gitlawb:mainfrom
GautamBytes:feat/plugin-marketplace

Conversation

@GautamBytes

@GautamBytes GautamBytes commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a local-first plugin marketplace across CLI and /plugins in the TUI
  • Support official, user, and trusted project catalogs with signed cache/update flows
  • Harden installs with fail-closed lock handling, hash verification, safe paths, atomic writes, and root locking

Closes #652

Summary by CodeRabbit

  • New Features
    • Added local-first plugin marketplace workflows (scope-aware catalog add/list/browse, validate, sign/verify, install/update) with release selection and safe trust gating.
    • Added zero plugins lifecycle commands: enable/disable and pin/unpin (including JSON output and restart-required behavior in the interactive UI).
    • Added an interactive /plugins manager with search, marketplace/catalog browsing, risk/integrity details, and disabled-plugin quarantine visibility.
  • Documentation
    • Expanded plugin marketplace and marketplace catalog docs (schema, signing rules, caching/stale behavior, quarantine/lock semantics).
  • Bug Fixes / Reliability
    • Improved integrity enforcement: tampered or mismatched managed plugins are blocked with integrity diagnostics; stale/invalid catalog handling is more predictable.
  • Tests
    • Added extensive CLI, unit, and TUI integration coverage for marketplace flows, install/update/verify, enable/disable, pin/unpin, and snapshot/restart logic.

Copilot AI review requested due to automatic review settings July 10, 2026 22:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0343c2b5-a4c9-47cf-bf7f-6c09fbb73a06

📥 Commits

Reviewing files that changed from the base of the PR and between f6b982b and 54dc2b3.

📒 Files selected for processing (2)
  • internal/tools/exec_command.go
  • internal/tools/exec_command_test.go

Walkthrough

This change adds a local-first plugin marketplace with catalog validation, signatures, caching, managed installation, lifecycle controls, integrity enforcement, CLI workflows, and a /plugins TUI manager.

Changes

Plugin marketplace and management

Layer / File(s) Summary
Marketplace contracts and registry
internal/marketplace/*, docs/PLUGIN_MARKETPLACE.md, README.md, AGENTS.md
Adds catalog schemas, validation, signatures, scoped registries, caching, source fetching, and marketplace documentation.
Managed plugin runtime
internal/plugins/*
Adds catalog-aware installs, lock metadata, quarantine state, integrity checks, atomic writes, lifecycle operations, and platform-specific root locks.
CLI workflows
internal/cli/*
Adds marketplace, browse, install, info, update, verify, enable/disable, pin/unpin commands with scope and verification handling.
TUI plugin manager
internal/tui/*
Adds /plugins, an interactive manager overlay, asynchronous command execution, marketplace actions, and modal input routing.
Plugin snapshots and execution reliability
internal/zerocommands/backend_snapshots.go, internal/tools/exec_command.go
Adds managed plugin snapshot metadata and waits for delayed process termination after interrupted command input.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PluginManager
  participant CLI
  participant Marketplace
  participant PluginRuntime
  User->>PluginManager: Select marketplace install
  PluginManager->>CLI: Run plugins install with scope and confirmation flags
  CLI->>Marketplace: Load and verify catalog release
  Marketplace-->>CLI: Release metadata and source
  CLI->>PluginRuntime: Install and record lock metadata
  PluginRuntime-->>CLI: Installation result
  CLI-->>PluginManager: Snapshot and restart status
  PluginManager-->>User: Render installed plugin state
Loading

Possibly related PRs

Suggested reviewers: Vasanthdev2004, gnanam1990, anandh8x

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also includes an unrelated exec-session interrupt handling fix in internal/tools, which is outside the marketplace scope. Split the exec-session fix into a separate PR, or link it to an issue if it must ship here.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing a local-first plugin marketplace.
Linked Issues check ✅ Passed The changes add marketplace catalogs and CLI/TUI flows for browse, install, info, update, verify, pin, and management, matching the linked issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/tui/model.go (1)

1329-1374: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cancel the in-flight plugin command on Esc. handlePluginManagerKey only clears m.pluginManager; call m.cancelPluginCommand() first so /plugins ... and manager actions can actually be interrupted, like the MCP branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/model.go` around lines 1329 - 1374, Update the Esc handling
branch for m.pluginManager to call m.cancelPluginCommand() before clearing the
manager state, matching the cancellation behavior in the MCP branch and ensuring
in-flight /plugins commands and manager actions are interrupted.
🧹 Nitpick comments (1)
internal/marketplace/registry_test.go (1)

25-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Verify all persisted fields in the round-trip test.

TestRegistryLoadSaveRoundTrip sets PublicKeyPath and VerificationStatus but only asserts ID and Source after reload. Adding assertions for the persisted fields would catch serialization bugs.

✅ Suggested additions
 	if len(loaded.Catalogs) != 1 || loaded.Catalogs[0].ID != "team" || loaded.Catalogs[0].Source != "./catalog.json" {
 		t.Fatalf("unexpected registry: %#v", loaded)
 	}
+	if loaded.Catalogs[0].PublicKeyPath != "./catalog.pub" {
+		t.Fatalf("publicKeyPath = %q, want ./catalog.pub", loaded.Catalogs[0].PublicKeyPath)
+	}
+	if loaded.Catalogs[0].VerificationStatus != VerificationUnsigned {
+		t.Fatalf("verificationStatus = %q, want %q", loaded.Catalogs[0].VerificationStatus, VerificationUnsigned)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/marketplace/registry_test.go` around lines 25 - 47, Update
TestRegistryLoadSaveRoundTrip to assert that the reloaded catalog’s
PublicKeyPath and VerificationStatus match the values originally provided, in
addition to the existing ID and Source checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 295-300: Clarify the marketplace documentation sentence so that
--allow-unverified is explicitly required only for installs from unsigned
catalogs, while --yes remains universally required; update the wording in the
Marketplace catalogs section of AGENTS.md.

In `@internal/cli/app.go`:
- Around line 891-906: Thread the TUI command context through the plugin
execution path: update tuiPluginCommand to pass ctx into runPlugins, then
propagate it through runPlugins and all marketplace browse/install/update
helpers, replacing their hard-coded context.Background() calls with the received
context. Preserve existing behavior for non-TUI callers by supplying an
appropriate background context at the entry point.

In `@internal/cli/plugin_marketplace.go`:
- Around line 1172-1220: Update compareSemver to apply SemVer prerelease
precedence after comparing the numeric core: when cores match, treat a version
without a prerelease identifier as greater than one with a prerelease
identifier, and only use a deterministic tie-breaker for equivalent prerelease
status. Adjust semverCore or add a helper near compareSemver to preserve
prerelease information, ensuring selectRelease chooses the stable release over
prereleases when --version is omitted.
- Around line 861-888: Bound each catalog operation in resolveMarketplacePlugin
with a per-catalog timeout context before calling loadCatalogForRegistration,
updateCatalogCache, or loadCatalogEntry. Derive the context from the command
context, use the existing catalog timeout convention if available, and ensure
each cancel function is released. Pass the timed context through all git-backed
loads so stalled remotes return an error instead of blocking the command
indefinitely.

In `@internal/marketplace/catalog.go`:
- Around line 271-285: Update ValidateRemoteCatalogReleaseSources to explicitly
reject file:// repository URLs in addition to CatalogSourceLocal paths. Use the
parsed source’s identifying fields or the original release.Repository value to
detect the file scheme, and return the same field-specific validation error;
ensure file:// sources cannot pass as CatalogSourceGit.
- Around line 481-485: Add table-driven tests for the HTTP source handling in
the catalog parsing test suite, covering loopback hosts 127.0.0.1, ::1,
::ffff:127.0.0.1, and 0.0.0.0 as accepted, plus localhost.example.com as
rejected. Exercise the relevant catalog parsing function and assert each case’s
success or error outcome.

In `@internal/marketplace/registry.go`:
- Around line 293-305: Add a default 30-second timeout in fetchGitCatalog when
the supplied context has no deadline, using context.WithTimeout and ensuring the
derived cancel function is deferred; preserve any existing caller-provided
deadline and continue passing the resulting context to exec.CommandContext.

In `@internal/plugins/root_lock_windows.go`:
- Around line 45-63: Update recoverStalePluginRootLock to perform a real Windows
process-liveness check using syscall.OpenProcess with
PROCESS_QUERY_LIMITED_INFORMATION before removing a stale lock; retain the lock
when the recorded PID is still running, handle invalid or unreadable PIDs
safely, and verify the API/constants are supported by the project’s Go
toolchain.

In `@internal/tui/plugin_manager.go`:
- Around line 93-95: Closing the plugin manager must invalidate any in-flight
command before clearing the overlay. Update the Esc handler and
prefillPluginManagerCommand to call m.cancelPluginCommand() before setting
m.pluginManager = nil, ensuring pluginCommandSeq/cancellation state marks
pending results stale and prevents applyPluginCommandResultMessage from
reopening the manager or appending transcript rows.

In `@internal/zerocommands/backend_snapshots.go`:
- Around line 268-269: Update pluginQuarantined to detect both path segments
containing "/.disabled/" and paths beginning with ".disabled/"; normalize the
path with filepath.ToSlash and combine the existing strings.Contains check with
a strings.HasPrefix check.

---

Outside diff comments:
In `@internal/tui/model.go`:
- Around line 1329-1374: Update the Esc handling branch for m.pluginManager to
call m.cancelPluginCommand() before clearing the manager state, matching the
cancellation behavior in the MCP branch and ensuring in-flight /plugins commands
and manager actions are interrupted.

---

Nitpick comments:
In `@internal/marketplace/registry_test.go`:
- Around line 25-47: Update TestRegistryLoadSaveRoundTrip to assert that the
reloaded catalog’s PublicKeyPath and VerificationStatus match the values
originally provided, in addition to the existing ID and Source checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b3bb943b-6487-4ca7-ad83-b4cf1b2a75c3

📥 Commits

Reviewing files that changed from the base of the PR and between 1af5882 and 61b7ad2.

📒 Files selected for processing (41)
  • AGENTS.md
  • README.md
  • docs/HOW_ZERO_WORKS.md
  • docs/PLUGIN_MARKETPLACE.md
  • internal/cli/app.go
  • internal/cli/distribution.go
  • internal/cli/extensions.go
  • internal/cli/hook_dispatch.go
  • internal/cli/marketplace_cli_test.go
  • internal/cli/marketplace_install_cli_test.go
  • internal/cli/plugin_disable_cli_test.go
  • internal/cli/plugin_marketplace.go
  • internal/cli/plugin_pin_cli_test.go
  • internal/marketplace/catalog.go
  • internal/marketplace/catalog_test.go
  • internal/marketplace/registry.go
  • internal/marketplace/registry_test.go
  • internal/plugins/disable_test.go
  • internal/plugins/install.go
  • internal/plugins/install_test.go
  • internal/plugins/integrity_test.go
  • internal/plugins/plugins.go
  • internal/plugins/root_lock_unix.go
  • internal/plugins/root_lock_unix_test.go
  • internal/plugins/root_lock_windows.go
  • internal/tui/autocomplete.go
  • internal/tui/clipboard.go
  • internal/tui/commands.go
  • internal/tui/commands_test.go
  • internal/tui/composer.go
  • internal/tui/files_panel.go
  • internal/tui/model.go
  • internal/tui/mouse.go
  • internal/tui/options.go
  • internal/tui/plan_step_detail.go
  • internal/tui/plugin_manager.go
  • internal/tui/scroll_test.go
  • internal/tui/sidebar.go
  • internal/tui/transcript_selection.go
  • internal/zerocommands/backend_snapshots.go
  • internal/zerocommands/backend_snapshots_test.go

Comment thread AGENTS.md
Comment thread internal/cli/app.go
Comment thread internal/cli/plugin_marketplace.go
Comment thread internal/cli/plugin_marketplace.go
Comment thread internal/marketplace/catalog.go
Comment thread internal/marketplace/catalog.go
Comment thread internal/marketplace/registry.go
Comment thread internal/plugins/root_lock_windows.go
Comment thread internal/tui/plugin_manager.go
Comment thread internal/zerocommands/backend_snapshots.go Outdated
@GautamBytes GautamBytes marked this pull request as draft July 10, 2026 22:49
@GautamBytes GautamBytes marked this pull request as ready for review July 10, 2026 23:09

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed this against current main — build, vet, gofmt, and the marketplace/plugins/cli tests all pass. The security model is sound and fail-closed: ed25519 catalog signatures are verified, invalid or missing signatures are rejected (unsigned needs an explicit --allow-unverified), install never executes fetched content, copyTree skips .git and refuses symlinks, subdir and plugin ids are validated against path traversal, the tree hash is checked against the catalog before publish, project-scoped catalogs are gated behind workspace trust, and install/update require --yes. It composes with the existing activation + permission gating rather than bypassing it.

Holding for two owner calls, both kevin's. First, issue #652 is still open with no maintainer sign-off — this is a sizable feature and I'd want your go-ahead on the scope before it lands. Second, the official catalog ships a hardcoded ed25519 public key and points at Gitlawb/zero-plugins, which doesn't exist yet. That trust anchor and repo are yours to provision and sign, so I don't want to merge a built-in "official" catalog whose signing key a contributor chose — until that repo exists and is signed with a key you control, zero plugins browse against the official catalog will fail. Minor non-blocker: the root lock removes its lockfile on release (the usual flock+remove race), fine for a low-concurrency per-user plugins dir.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@gnanam1990 when you have time, #653 is a big one worth a second pair of eyes — a local-first plugin marketplace (+7946, 43 files) with ed25519 catalog signing, git/HTTP catalog fetch, hash-verified installs, and workspace-trust gating for project scope. I reviewed it inline and the security model looks fail-closed and sound, but I'd value your independent read on the install/verify path (signature enforcement, copyTree symlink/.git handling, subdir path safety) before it goes anywhere. I'm holding it for an owner call on issue #652 and the official-catalog trust anchor, not on the code.

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.

Add a local-first plugin marketplace

3 participants