---
title: Plugin Marketplace (part 2)
---

# Plugin Marketplace (part 2)

## What it is

This is part 2 of the [Plugin Marketplace](plugin-marketplace.md) page. That page covers what
the Marketplace is, where to find it, and the opening of the numbered walkthrough: browsing the
catalogue, installing and uninstalling, the update check, version compatibility, the gate on
third-party plugin backends, and an agent’s install request.

This half carries the deeper material: what a plugin may read from your own data and what it can
watch you doing, ratings and reviews, the errors you can hit, the plugin-author surfaces,
publishing and package signing, and how the registry, the install and the IPC layers actually
work. The numbered walkthrough continues from step 9 below.

## Where to find it

The same two surfaces as the [main page](plugin-marketplace.md): the Marketplace virtual project
in the sidebar for browsing, and **Settings → Plugins** for what you have installed. Most of
this half is reached from a single plugin’s own detail page or its Settings card, with the
**Plugin Developer** view for the plugin authors themselves.

## How it behaves

### Your data a plugin can ask to read

Some plugins can ask to read your **past Omniscio sessions and projects** — for example, a plugin that turns your existing work into a summary or a deck. This is gated behind the `sessions.readHistory` permission and a strict, opt-in privacy model:

- **Nothing by default.** A plugin reads none of your history until you grant it. When a plugin needs access, a picker opens and **you choose exactly which projects or sessions to hand over** — it can only ever read those (a project you pick covers its sessions).
- **Text only.** The plugin receives the plain-text conversation, never the raw internals — tool output, file contents, and anything that could carry secrets are stripped out before it reaches the plugin.
- **Logged and revocable.** Every read is recorded in a log the plugin cannot touch, and your grant persists until you revoke it. Review what a plugin was granted, see its read log, and **revoke access** anytime under **Settings → Plugins → "Session history access"** (shown per plugin, only once a plugin has requested access).

**Boards are a separate ask, on purpose.** A plugin that wants to read your PM **boards** requests a
distinct `boards.read` permission rather than reusing the session-history one, because a board is
project-management data and not a conversation. Agreeing to share past sessions should never quietly
hand over something else. It follows the same model — nothing by default, you pick which boards, every
read logged, revocable — with two differences worth knowing: **sharing one board shares only that
board** (boards sit inside a workspace, and a workspace is far wider than the thing you pointed at),
and the plugin never receives the board's items, only a summary. Board grants appear in the same
per-plugin access panel as the others, each labelled for what it is.

A first-party built-in plugin shows no install-time consent dialog, so for those the picker and this access panel are the control surface. (Plugin authors: the developer side — the `sessionHistory` bridge namespace — is in [Plugin Bridge Capabilities](plugin-bridge-capabilities.md).)

**Plugin authors:** the marketplace publish validator keeps its OWN mirror of the declarable
permission set, because the cloud function cannot import host code. If the host gains a permission and
that mirror is not updated, an honest manifest declaring it is rejected at publish with
`Unknown permissions: <name>` — before a submission even exists, so no reviewer can wave it through.
That mirror is parity-locked to the host; see
[marketplace-permission-parity-contract.md](../../.claude/memory/contracts/marketplace-permission-parity-contract.md).

### What a plugin can see you doing

Separate from reading your past work, a plugin can ask to see your sessions **as they happen**.
Two different things, with two different answers:

- **A companion or status overlay** — a plugin that reacts to your work (a pet, an activity HUD)
  can ask for `sessions.observeStatus`, shown in the consent dialog as **Session Activity**. It
  sees only _when_ a session starts, finishes, or needs you. It never sees a single word of what
  you or the agent wrote. A plugin that has not asked for it receives nothing at all, even if it
  has an overlay on screen.
- **A plugin watching a session it started itself** — if a plugin launched a session for you, it
  can follow that session's output live. That needs no extra permission, because it is the
  plugin's own work: it can only ever see sessions it launched, never yours or another plugin's.
  If you revoke that project's folder access, the stream stops.

### Rating

9. **View ratings.** Each detail page shows the average star rating, total review count, and a star-distribution histogram. The latest 20 reviews load below; the **Load more** button paginates 20 at a time. The star row under the plugin's name is that **average**, not your own rating — so when you are able to rate, the whole row becomes one control that jumps you down to the Ratings & Reviews form and puts the cursor in it. It stays a plain display when there is no form to reach (signed out, or the plugin is not installed), so it can never be a click that does nothing. It does not pre-pick a star: five stars that mean "the community said 4" in one place and "I say 4" in another would be two things wearing one face, and splitting the row into five buttons would turn a single spoken label into five for a screen reader.
10. **Submit a rating (signed-in installers only).** Submitting needs the **marketplace GitHub sign-in** — `MARKETPLACE_SUBMIT_RATING` refuses unless `getAdminStatus()` is `authenticated` — so the rating box ([PluginRatingBox.tsx](/src/renderer/src/features/marketplace/PluginRatingBox.tsx)) re-checks that status every time the page opens and shows only what can actually work: It is **not** your Omniscio account: an earlier implementation sent `authService.getActiveAccountId()` as an `x-user-id` header, the endpoint ignored that header, and every submission failed — that code is gone.
    - **Checking** — nothing, until the status answers, so a stale value never flashes the form.
    - **Signed out** (or the check failed) — builds with the in-app sign-in say to sign in with GitHub on the **My Plugins** tab; release builds, where that sign-in is compile-stripped, say reviews aren't available in this version yet. A release build holding an `amc-plugin login` credential counts as signed in ([marketplace-admin-auth-contract.md](/.claude/memory/contracts/marketplace-admin-auth-contract.md) I11, I12). The Ratings & Reviews card itself still renders either way, so a build that cannot submit says why instead of leaving a silent gap.
    - **Signed in, not installed** — "Install this plugin to leave a review."
    - **Signed in and installed** — the star form, with the green **Installed** verified badge on the ratings header. Pick 1–5 stars, optionally add a short review (no links), click **Submit Rating**.

    The server keys the rating to the verified marketplace Firebase uid from that GitHub sign-in (never your Omniscio or Anthropic account) and refuses a plugin's own author (`403`, "You cannot review your own plugin").
11. **Edit your rating.** The backend keys each rating by your identity and **upserts** — you have exactly one rating per plugin, and re-submitting replaces it rather than stacking a new one. When you've already rated a plugin this session the form is honest about that: it pre-fills your stars + review and the button reads **Update your rating** instead of Submit. The public ratings feed pseudonymizes rater ids (a review can't be traced back to "you" from the list), so this pre-fill is driven by a session-tracked map of your own submissions (`myRatings` in the marketplace store), cleared on cloud logout so a different signed-in user never inherits it.

### Errors

- **Registry can't be fetched** — the card shows a red banner with the error text and a **Retry** button.
- **Single install fails** (network glitch, checksum mismatch, oversized file) — red toast quotes the reason; your existing install, if any, is left untouched (see "atomic" below).
- **Rating submit fails** — toast with the reason; nothing persists locally, and your typed review is kept so a retry does not start from blank.
- **A rating the server REFUSES says which rule you hit.** Reviewing a plugin you published (403 `FORBIDDEN`), the per-user rate limit, a link in the review text, an out-of-range star value — each comes back as a 4xx carrying the marketplace's own sentence, and that sentence is what you read: *"You cannot review your own plugin"*, not a guess. It is carried as its own error type (`MarketplaceRefusedError`, thrown by [marketplace-ratings.ts](/src/main/services/marketplace/marketplace-ratings.ts) and passed through by TYPE in [marketplace-user-errors.ts](/src/main/services/marketplace/marketplace-user-errors.ts)), the same way an upgrade-required refusal already was — because left to the classifier it arrives as the string `HTTP 403: {…}`, matches the transport bucket's `/\bhttp\s+\d{3}\b/`, and becomes *"Couldn't reach the plugin marketplace. Check your internet connection and try again."* The marketplace had answered perfectly; the advice was wrong, retrying could never clear it, and the explanation was thrown away. Only a **4xx carrying a server sentence** is passed through — a 5xx is a genuine upstream fault and keeps the transport copy, and a non-JSON, blank or over-long body falls back to it too. The raw status and body still reach the log.

**A failure that retrying cannot fix says so.** Raw causes are classified into plain sentences by `toMarketplaceUserError` ([marketplace-user-errors.ts](/src/main/services/marketplace/marketplace-user-errors.ts)), and a deterministic failure never gets the generic "Please try again" — that advice sends the user round a loop that fails identically every time. A full disk asks them to free space; a directory another process holds open (`EPERM`/`EBUSY`, and `safeSwapInstalledDir`'s own "currently in use" refusal) asks them to close what's using the plugin; and a plugins folder Omniscio cannot write to (`EXDEV`/`EROFS`) states plainly that retrying won't help and asks them to report it. These fs checks run **before** the corrupt-package check on purpose: an fs error quotes the path it failed on, so a locked `…/manifest.json` matches the corruption heuristic and would otherwise blame a package that was never damaged.

That last bucket also decides what reaches Sentry. A full disk or a locked file is the user's environment, so it logs WARN and is marked expected, like a network blip. Failing to write our _own_ plugins folder is a defect, so it stays unmarked at ERROR — the [EXDEV cross-device install bug](#how-it-works) produced no crash signal at all precisely because it hid in the unmarked generic bucket.

**A reply bigger than Omniscio accepts says exactly that — never "corrupted".** When the catalog, a plugin's details or ratings, or a package download is over its size cap (see "Fetch hardening" below), the user reads *"The plugin marketplace sent more data than Omniscio can accept, so this couldn't be loaded. The plugin isn't damaged, and trying again won't help until we fix it on our end."* — translated, and with no retry advice, because the same reply fails the same way until the server changes. This check runs **before** the corrupt-package check and matches only the size-cap errors themselves (`Response too large` / `Registry response too large` / `Package too large`), so an unrelated "too large" such as an HTTP 413 keeps its old bucket. It stays unmarked at ERROR: an oversized reply is our defect, and its Sentry report is how the 2026-09-23 incident was found — GitHub Integration's details had grown to 299,361 bytes and every user opening its page was told the plugin looked corrupted.

### Developer dashboard

The **Plugin Developer** view (a separate virtual project for plugin authors) is where plugin authors track their own submissions to the marketplace. It shows:

- **Sign-in button** — OAuth flow against the marketplace Firebase project. Internal/dev builds only: the in-app GitHub sign-in handler is compile-stripped from the packaged app, so a release build shows a notice pointing at the CLI instead.
- **Submissions list** — each submission is an **expandable status card** (`SubmissionStatusCard`). Collapsed, it shows plugin id + version, a status `Pill` (**Pending review** / **Published** / **Changes requested**), and the relative submission time. Expanded, it reveals a three-step **status timeline** (Submitted → In review → Decision, each marker coloured by state and stamped with a relative time), the full **What's new** changelog, **Reviewer notes** (shown for an approved submission with feedback as well as a rejection), and a **what-happens-next** hint tailored to the status.

**In a release build, the tab signs in with your `amc-plugin` login.** A packaged app has no marketplace sign-in of its own, so it borrows the credential `amc-plugin login` already wrote to `~/.amc/marketplace-token` — the same account that ran `amc-plugin publish`, so everyone who has a submission to show already has this file. Nothing new to sign into: the **My Plugins** tab lists your submissions as soon as the CLI is logged in, `amc-plugin logout` signs the tab out too (a refreshed token is written back to the CLI's file, never copied into the app), and a borrowed login is always treated as a **developer**, never an admin — it can list your own submissions but cannot open any review surface. The app's own token, when one exists (internal builds), still wins ([marketplace-cli-token.ts](/src/main/services/marketplace/marketplace-cli-token.ts); the borrowed-CLI-credential rule in [marketplace-admin-auth-contract.md](/.claude/memory/contracts/marketplace-admin-auth-contract.md)).

The view exists separately from the **Marketplace Review** view used by Omniscio admins to triage incoming submissions.

## For agents

### Who may submit (the publisher gate)

**Signing in is not enough to publish.** Uploading is refused unless the account is an **approved publisher**. Being signed in only proves you hold a valid login; before this gate existed, any GitHub account could put third-party code into the admin review queue, and the review queue was the only thing between a stranger and users.

- **Refusal is a 403** with a plain-English message, raised _before_ the package is parsed — an unapproved account never gets its upload read or stored.
- **A first refusal records an application.** The developer's real identity (GitHub handle, display name, email) is written once so an admin can see who is asking; repeated attempts after that cost nothing and write nothing.
- **Admins always pass.** A `role: 'admin'` account publishes regardless of its publisher status — the floor that makes the gate safe to roll out in any order.
- **The same gate covers automations.** `uploadAutomation` accepts a definition rather than a zip, but it is third-party code by another name and carries the identical check — see [automation-marketplace.md](automation-marketplace.md).
- **Blocking someone does not unpublish their existing plugins.** It stops future submissions only; takedown is the separate unapprove action.

Admins manage this through two admin-only endpoints — `getPublishers` (who is waiting) and `setPublisherStatus` (approve / block). There is **no in-app screen for it yet**; approving a publisher is an API call today. Full invariants: [marketplace-publisher-gate-contract.md](/.claude/memory/contracts/marketplace-publisher-gate-contract.md).

### Package signing (built, not yet switched on)

Every published package is meant to carry an Ed25519 signature so the app can prove the file it downloaded is the one that was reviewed. The machinery exists on both sides — the server signs at upload, the catalogue serves the signature, and the app knows how to verify it, including key rotation. **It is not switched on yet: no signing key exists, so nothing is signed today.**

- **A deploy without the key now FAILS.** The signing secrets are declared and bound to the upload function, so signing can no longer be "switched on" by accident and silently do nothing — which is exactly what would have happened before, because nothing ever fed the key to the function.
- **An upload that cannot be signed is refused**, with a retryable error, _before_ anything is stored — no half-uploaded file, no reserved version left behind.
- **Approving a plugin can no longer un-sign it.** The publish step rewrites the version record wholesale, so it now carries an existing signature forward.
- **Already-published packages are signed by a script**, not by re-uploading. It signs only bytes it can verify against the checksum recorded at publish time, and refuses anything it cannot prove.

**Turning it on has a strict order**, and getting it wrong breaks installs: bind (done) → create the key → deploy → back-sign the catalogue → pin the public key in a client release → wait for the fleet to upgrade → only then enforce. Enforcing before the fleet has upgraded breaks every install on every un-upgraded client.

**Known gap:** 5 published versions have no recorded checksum in any record, so their bytes cannot be proven and they will not be signed. (Two more that looked unrecoverable turned out to still have their checksum on the original submission — the same hash written at upload, simply never copied across — and are now recovered legitimately.)

Of those 5, only **two need action**: `repoguard` and `virtual-pets` are the only version of their plugin, so they would become uninstallable once strict checking is on. The rest are superseded by later versions, or retired.

Re-publishing those two is blocked by code, not by their authors — both are recorded as owned by a placeholder account nobody can sign in as, so any real developer's upload is refused, and the version has to be bumped because the endpoint will not overwrite one that already exists. **Existing installs are unaffected** either way: strict checking only applies when installing or updating.

Full invariants and current counts: [marketplace-package-signing-contract.md](/.claude/memory/contracts/marketplace-package-signing-contract.md).

### How it works

**Registry source.** The catalog is served by AMC's own backend at `https://amcback.jls.dev/marketplace` with three GET endpoints (it ran as Firebase Cloud Functions until 2026-07-31; every endpoint kept its exact name as a path segment, so only the base URL changed). The Cloud Functions are **not** retired: the hosted sign-in page deliberately still calls them, because amc-back's CORS preflight returns no `Access-Control-Allow-Origin` and a browser cannot reach that mount ([contract](/.claude/memory/contracts/automation-marketplace-contract.md)). The three GET endpoints:

- `GET /getRegistry` — full plugin catalog
- `GET /getPluginDetail/<pluginId>` — single-plugin metadata (changelog, permissions, etc.). Its version entries deliberately carry **no** `checksums` — installs verify against the registry, and shipping every version's per-file map is what pushed a reply past the client's cap ([contract](/.claude/memory/contracts/marketplace-checksum-contract.md) `detail-reply-carries-no-checksums`)
- `GET /downloadPlugin/<pluginId>/<version>` — signed-URL redirect to the `.amcplugin` package
- `GET /getRatings?pluginId=<id>&limit=<n>&offset=<n>` — paginated user reviews
- `POST /submitRating` (requires `x-user-id` header) — submit or replace a rating

The registry response shape is `{ plugins: MarketplacePlugin[], generatedAt: string }`. The cloud function enforces `PluginCategory` at publish-time so the renderer can treat the wire shape as `MarketplacePlugin` 1:1.

**Auth pipeline (developer + rating endpoints).** OAuth flows through the marketplace Firebase project (`auth.<...>.firebaseapp.com` callback). The `MARKETPLACE_ADMIN_AUTH` IPC handler kicks off the flow; on return the desktop client redeems the completed sign-in by polling `getAuthSession` with the session id in the **`X-Auth-Session` header — never a query-string param**, because that id is a bearer capability and a URL lands it in access logs. The response carries a `refreshToken` so the long-lived token can be persisted. The credential doc itself (`auth_sessions/<id>`) is locked `allow read, write: if false`, so `getAuthSession` is the only sanctioned redemption route — reading it directly returns `403` on every poll (the 2026-08-19 sign-in outage). See [marketplace-admin-auth-contract.md](/.claude/memory/contracts/marketplace-admin-auth-contract.md). For rating submission, the server keys the rating doc and its per-user rate limit to the **verified Firebase uid it decodes from the Bearer token**, and deliberately ignores any client-supplied identity header (anti-fraud). An earlier client sent the active Omniscio account id as an `x-user-id` header; the endpoint discarded it, so every rating 401'd. Submission therefore needs the same marketplace GitHub token the developer endpoints use, and because `MARKETPLACE_ADMIN_AUTH` is registered only under `INTERNAL_ONLY` it does not exist in a packaged build. That is why the submission UI is gated on `MARKETPLACE_DEVELOPER_AUTH_AVAILABLE`, locked by [marketplace-admin-auth-build-gate.test.ts](/tests/unit/lint/marketplace-admin-auth-build-gate.test.ts).

**Fetch hardening.** Every HTTP call goes through `fetchWithTimeout()` ([src/main/services/marketplace/marketplace-service.ts](/src/main/services/marketplace/marketplace-service.ts)) which enforces a 15-second timeout (AbortController), checks `Content-Length` against the size cap before the body streams, and re-checks the actual byte count after download (catches chunked transfers that omit `Content-Length`). The registry, plugin-detail and ratings responses are each capped at **256 KB**; each plugin package at **10 MB**. If the function returns HTML instead of JSON (rate-limited, deploy in progress), Omniscio catches the JSON parse error and surfaces a useful preview like `Registry response is not valid JSON: "<!doctype html>…"` instead of a cryptic SyntaxError.

**Checksum verification.** When a plugin's registry entry includes a `checksums` map (the only mode the official registry uses), Omniscio verifies it before writing the directory to disk. The map holds two kinds of entries: a **per-file** hash for each file inside the `.amcplugin` package (verified against the extracted file), and an optional **package-level** entry under the well-known key `package.sha256` — the SHA-256 of the _entire downloaded package_, verified against the whole download rather than treated as a file inside the zip. A mismatch of either kind aborts the install with `Checksum mismatch for <file>: expected abc123…, got def456…`; a per-file entry the package omits aborts with `Checksum file missing: <file>` (so a compromised registry can't claim a file the bundle doesn't contain). Your existing install, if any, is untouched because the install was still in the temp directory. (`package.sha256` is a package-level fingerprint, not a file in the zip — mis-handling it as a missing file was the 2026-06-24 install-failure bug; see [marketplace-checksum-contract.md](/.claude/memory/contracts/marketplace-checksum-contract.md).)

**Atomic install.** The `.amcplugin` package (a zip archive) downloads into `<userData>/.plugin-staging/<id>/`, gets extracted there, has its checksums verified, and only then is atomically renamed into place at `<userData>/plugins/<id>/`. If an existing install is being updated, Omniscio removes it (`rmSync` recursive) immediately before the rename. On any failure mid-install, the staging directory is cleaned up best-effort and your previous install is preserved.

Staging deliberately lives **under the user-data dir, not `os.tmpdir()`** — and as a _sibling_ of `plugins/`, never inside it. The commit step is a `rename()`, which cannot cross filesystems, so staging on the system drive broke every install and update with `EXDEV: cross-device link not permitted` on any machine whose user-data dir sits on a different drive (a data dir relocated to `H:\` while `TEMP` stays on `C:\`). Because the swap aborts cleanly, the symptom was an update that failed with "Something went wrong with the plugin marketplace" while the old version stayed installed — for every plugin, not one. Keeping staging a sibling matters too: a staging dir holds a real `manifest.json`, so nested under `plugins/` a leftover would be counted as an installed plugin and then removed by the registry revocation sweep. The sweep now also requires a directory's NAME to be a legal plugin id, which catches a differently-named leftover such as an update backup — but **that is not a substitute for the sibling rule**, because a staging dir is named for the plugin id itself and so would still pass the name check. Keep staging outside `plugins/`.

**Cache.** The registry response is cached in-memory for 5 minutes (`CACHE_TTL_MS`) to avoid hammering Firebase when you open and close the marketplace. Install and uninstall both invalidate the cache so the very next fetch reflects the new state. Push event `PLUGIN_REGISTRY_CHANGED` (with `{ pluginId, installed }`) is emitted on every install/uninstall, alongside `PROJECTS_CHANGED` and `DIVIDERS_CHANGED` so the sidebar reflects the new virtual project immediately.

**IPC surface.** Seven handlers expose the service to the renderer:

- `MARKETPLACE_FETCH_REGISTRY` — returns `MarketplacePluginStatus { plugin, installed, installedVersion, updateAvailable }[]`
- `MARKETPLACE_INSTALL` — install or update by `pluginId`; also adds to `enabledPlugins` and restores/creates the virtual project
- `MARKETPLACE_UNINSTALL` — remove the plugin's directory by `pluginId`; soft-deletes the virtual project, preserves plugin SQLite data
- `MARKETPLACE_GET_PLUGIN_DETAIL` — single-plugin detail (changelog, permissions, full description)
- `MARKETPLACE_GET_RATINGS` — paginated reviews (`{ pluginId, limit?, offset? }`)
- `MARKETPLACE_SUBMIT_RATING` — submit or replace your rating (`{ pluginId, stars, review? }`); refused unless the marketplace GitHub sign-in is `authenticated`, and the server keys the rating to that verified Firebase uid
- `MARKETPLACE_GET_MY_PLUGINS` — the signed-in developer's own submissions. The handler first checks `getAdminStatus()`; only when `status === 'authenticated'` does it call `fetchMySubmissions()` ([marketplace-review-service.ts](/src/main/services/marketplace/marketplace-review-service.ts)), which hits the auth-scoped `/getMyPlugins` cloud function (`developer.uid == request.auth.uid`) with the OAuth bearer token. A signed-out developer gets a benign `[]` _without_ a network call — the renderer decides whether to show the "Sign in with GitHub" prompt from `MARKETPLACE_ADMIN_STATUS`, so the empty array is never mistaken for an authenticated-but-empty result. That status channel is registered in the same shipped [marketplace-handlers.ts](/src/main/ipc/marketplace-handlers.ts), **not** in the compile-stripped review handlers — while it lived there, a release build's tab could never see itself as signed in, whatever identity the main process held. Each row is a `MySubmission` ([types-marketplace-review.ts](/src/shared/types-marketplace-review.ts)) — a lean subset of the reviewer's `ReviewSubmission` enriched with `changelog`, `packageSize`, `reviewedAt`, and `reviewNotes`. The **My Plugins** tab renders these through `SubmissionStatusCard` ([SubmissionStatusCard.tsx](/src/renderer/src/features/marketplace/components/SubmissionStatusCard.tsx)) — an expandable status-detail card. The collapsed header carries plugin id + version, a status `Pill`, and a relative submitted time; expanding reveals a status timeline, the full changelog, reviewer notes (surfaced for an approved-with-feedback submission too, not just rejections), and a status-specific next-step hint. The pure timeline/step logic lives in [submission-status.ts](/src/renderer/src/features/marketplace/lib/submission-status.ts) (`getSubmissionSteps` / `shouldShowReviewNote`), unit-tested in [marketplace-submission-status.test.ts](/tests/unit/marketplace-submission-status.test.ts).

The admin-side review handlers (approve/reject/sandbox/claim/source-tree, 11 total) live in [src/main/ipc/marketplace-review-handlers.ts](/src/main/ipc/marketplace-review-handlers.ts).

**Files.**

- Backend service — [src/main/services/marketplace/marketplace-service.ts](/src/main/services/marketplace/marketplace-service.ts) (fetch, install, uninstall, detail, ratings, cache)
- IPC handlers — [src/main/ipc/marketplace-handlers.ts](/src/main/ipc/marketplace-handlers.ts)
- IPC channel names — [src/shared/ipc-channels/index.ts](/src/shared/ipc-channels/index.ts) (`MARKETPLACE_*`)
- Marketplace grid view — [src/renderer/src/features/marketplace/MarketplaceView.tsx](/src/renderer/src/features/marketplace/MarketplaceView.tsx) (Browse-tab update badge + banner; launch/interval/focus check lives in [App.tsx](/src/renderer/src/App.tsx))
- Updates Review modal — [src/renderer/src/features/marketplace/PluginUpdatesModal.tsx](/src/renderer/src/features/marketplace/PluginUpdatesModal.tsx)
- Permission-diff helper — [src/shared/plugin-permissions.ts](/src/shared/plugin-permissions.ts) (`diffPermissions`)
- Trust-signal helper — [src/shared/marketplace-trust-signals.ts](/src/shared/marketplace-trust-signals.ts) (`isFirstPartyPlugin`, `summarizePluginTrust`, `SENSITIVE_PLUGIN_PERMISSIONS`)
- Plugin card — [src/renderer/src/features/marketplace/MarketplacePluginCard.tsx](/src/renderer/src/features/marketplace/MarketplacePluginCard.tsx)
- Plugin detail page — [src/renderer/src/features/marketplace/MarketplacePluginDetail.tsx](/src/renderer/src/features/marketplace/MarketplacePluginDetail.tsx)
- Shared re-approval callout — [src/renderer/src/components/ui/ReconsentCallout.tsx](/src/renderer/src/components/ui/ReconsentCallout.tsx) (Settings + card + detail; see [plugin-reconsent-surfacing-contract.md](/.claude/memory/contracts/plugin-reconsent-surfacing-contract.md))
- "Opened a dead plugin" toast — [src/renderer/src/features/plugins/usePluginReconsentToast.ts](/src/renderer/src/features/plugins/usePluginReconsentToast.ts) (fired from [Dashboard.tsx](/src/renderer/src/features/dashboard/Dashboard.tsx))
- Marketplace sidebar — [src/renderer/src/features/marketplace/MarketplaceSidebar.tsx](/src/renderer/src/features/marketplace/MarketplaceSidebar.tsx) (the SOLE home of the category filter) plus its footer — [MarketplaceSidebarFooter.tsx](/src/renderer/src/features/marketplace/MarketplaceSidebarFooter.tsx) (the four-state footer: installed count, status line + freshness stamp, the empty-state **Browse plugins** link, and the on-demand **Check for updates** button). The footer's success-only pulse (`.marketplace-check-flash`) is defined in [globals.css](/src/renderer/src/styles/globals.css).
- Browse toolbar + plugin grid — [src/renderer/src/features/marketplace/BrowseTab.tsx](/src/renderer/src/features/marketplace/BrowseTab.tsx) (the SOLE home of search + sort; the search box is the shared `SearchInput`, deliberately — see "One control, one home" above)
- Developer dashboard — src/renderer/src/features/plugin-developer/PluginDeveloperView.tsx
- Marketplace store — [src/renderer/src/stores/marketplace-store.ts](/src/renderer/src/stores/marketplace-store.ts)
- Developer store — [src/renderer/src/stores/plugin-developer-store.ts](/src/renderer/src/stores/plugin-developer-store.ts)
- Types — `MarketplacePlugin`, `MarketplacePluginStatus`, `PluginCategory` in [src/shared/types.ts](/src/shared/types.ts); `MarketplacePluginDetail`, `MarketplaceRatingsResponse` in [src/shared/types-marketplace.ts](/src/shared/types-marketplace.ts)
- Legacy Settings card (browse-only) — [src/renderer/src/features/settings/PluginSettings.tsx](../../src/renderer/src/features/settings/sections/plugins/PluginSettings.tsx)

## Related

[Plugin Marketplace](plugin-marketplace.md) is the first half of this page — what the
Marketplace is, how to browse it, and how installing, updating and the security gate work. The
contracts and key files listed above hold the invariants behind what this half describes.
