Skip to content

fix(core): surface sandboxed plugins in the admin plugin management API#1284

Open
eyupcanakman wants to merge 1 commit into
emdash-cms:mainfrom
eyupcanakman:fix/admin-list-sandboxed-plugins
Open

fix(core): surface sandboxed plugins in the admin plugin management API#1284
eyupcanakman wants to merge 1 commit into
emdash-cms:mainfrom
eyupcanakman:fix/admin-list-sandboxed-plugins

Conversation

@eyupcanakman
Copy link
Copy Markdown
Contributor

What does this PR do?

Plugins registered statically via sandboxed: [] in astro.config.mjs did not appear on the admin Plugins screen (/_emdash/admin/plugins). Trusted plugins: [] entries and marketplace installs showed up, but sandboxed plugins were invisible there even though their hooks, cron schedules, and sandbox isolation all kept working.

The four admin plugin handlers in packages/core/src/api/handlers/plugins.ts (handlePluginList, handlePluginGet, handlePluginEnable, handlePluginDisable) only read configuredPlugins and never the runtime's sandboxedPluginEntries, so the sandboxed: [] array fell through the gap between the configured list and the marketplace state rows.

The fix passes sandboxedPluginEntries to all four handlers and adds a buildSandboxedPluginInfo helper. The routes read it from locals.emdash, so the middleware now exposes sandboxedPluginEntries there and the EmDashHandlers type declares it (required, so it can not be dropped). Sandboxed plugins now list with source: "config" and a new sandboxed: true flag, and can be fetched, enabled, and disabled the same way trusted plugins: [] entries are. The runtime already gates sandboxed hook execution on isPluginEnabled, and the enable/disable routes propagate state through emdash.setPluginStatus, so toggling worked at the runtime level but could not be reached from the admin API. A list-only change would have shown a toggle that returns NOT_FOUND, so all four handlers move together.

A few notes for reviewers:

  • The sandboxed enable/disable branch mirrors the trusted-config branch exactly (stateRepo.enable/disable with no source override). An id that already has a leftover marketplace state row behaves the same on both config paths, so I left that case as-is rather than special-casing the sandboxed path.
  • The sandboxed flag is added to the core API response, matching the runtime manifest which already marks these entries. I did not add an admin UI badge; the admin PluginInfo type has lagged core additions before, and the flag is available for a follow-up if a badge is wanted.
  • handlePluginGet still returns NOT_FOUND for marketplace and registry plugins. That is pre-existing and outside the scope here.

A route-level test drives a real EmDashRuntime through the actual admin routes: the list route surfaces the sandboxed plugin and the enable/disable routes toggle it, and the same test returns 404 against the current code. A handler test covers a sandboxed entry surfacing in the list and the case where a configured plugin shadows one with the same id, where the configured entry wins and the row is listed once.

Closes #773

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion

AI-generated code disclosure

  • This PR includes AI-generated code - model/tool: Claude Opus 4.8

Screenshots / test output

$ pnpm --filter emdash exec vitest run tests/integration/api/plugins.test.ts tests/integration/astro/admin-plugins-sandboxed.test.ts
 Test Files  2 passed (2)
      Tests  3 passed (3)

Plugins registered statically via `sandboxed: []` in astro.config.mjs were absent from the admin Plugins screen.
The four admin plugin handlers only read configuredPlugins, never the runtime's sandboxedPluginEntries, so trusted `plugins: []` and marketplace plugins appeared while sandboxed ones did not.

Pass sandboxedPluginEntries to handlePluginList, handlePluginGet, handlePluginEnable, and handlePluginDisable, and add a buildSandboxedPluginInfo helper.
Sandboxed plugins now list with source "config" and a sandboxed flag, and can be fetched, enabled, and disabled like trusted config plugins.
The runtime already gates their hook execution on isPluginEnabled, so enable and disable were supported at the runtime level but unreachable from the admin API.

Closes emdash-cms#773
Copilot AI review requested due to automatic review settings June 2, 2026 08:10
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 2, 2026

🦋 Changeset detected

Latest commit: 59114a8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes admin plugin management so statically-sandboxed plugins (from sandboxed: [] in astro.config.mjs) appear in the Plugins UI and can be managed via the existing admin API.

Changes:

  • Add sandboxedPluginEntries to the Astro middleware/handler facade and thread it through admin plugin routes.
  • Extend plugin admin handlers to list/get/enable/disable statically-sandboxed plugin entries.
  • Add integration tests covering listing and enable/disable behavior for sandboxed entries.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/core/tests/utils/mcp-runtime.ts Test runtime handler facade now includes sandboxedPluginEntries.
packages/core/tests/integration/astro/admin-plugins-sandboxed.test.ts New end-to-end Astro route integration tests for sandboxed plugin listing and toggling.
packages/core/tests/integration/api/plugins.test.ts New handler-level test for listing sandboxed entries and configured-plugin shadowing.
packages/core/src/astro/types.ts Extends EmDashHandlers with sandboxedPluginEntries.
packages/core/src/astro/routes/api/admin/plugins/index.ts Passes sandboxed entries into handlePluginList.
packages/core/src/astro/routes/api/admin/plugins/[id]/index.ts Passes sandboxed entries into handlePluginGet.
packages/core/src/astro/routes/api/admin/plugins/[id]/enable.ts Passes sandboxed entries into handlePluginEnable.
packages/core/src/astro/routes/api/admin/plugins/[id]/disable.ts Passes sandboxed entries into handlePluginDisable.
packages/core/src/astro/middleware.ts Adds sandboxedPluginEntries to locals.emdash.
packages/core/src/api/handlers/plugins.ts Implements sandboxed plugin info + handler support for list/get/enable/disable.
.changeset/admin-list-sandboxed-plugins.md Patch changeset describing the fix and newly supported behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +61
// Role.ADMIN is 50 in @emdash-cms/auth; plugins:read / plugins:manage require it.
const admin = { id: "admin-1", role: 50 };
Comment on lines +66 to +74
function facade(runtime: EmDashRuntime) {
return {
db: runtime.db,
configuredPlugins: runtime.configuredPlugins,
sandboxedPluginEntries: runtime.sandboxedPluginEntries,
config: runtime.config,
setPluginStatus: runtime.setPluginStatus.bind(runtime),
};
}
Comment on lines +217 to +224
const sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);
if (sandboxed) {
const state = await stateRepo.get(pluginId);
return {
success: true,
data: { item: buildSandboxedPluginInfo(sandboxed, state) },
};
}
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 2, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1284

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1284

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1284

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1284

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1284

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1284

emdash

npm i https://pkg.pr.new/emdash@1284

create-emdash

npm i https://pkg.pr.new/create-emdash@1284

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1284

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1284

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1284

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1284

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1284

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1284

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1284

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1284

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1284

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1284

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1284

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1284

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1284

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1284

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1284

commit: 59114a8

Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is a clean, well-scoped bugfix. The approach is sound: sandboxed: [] config entries were missing from the admin plugin management API because the four plugin handlers only looked at configuredPlugins (trusted plugins) and _plugin_state rows (marketplace/registry). The fix threads sandboxedPluginEntries through all four handlers, adds a matching buildSandboxedPluginInfo helper, exposes the field via locals.emdash, and updates EmDashHandlers so the contract is compile-checked.

I checked:

  • Logic: All four handlers (list, get, enable, disable) are updated consistently. The deduplication logic in handlePluginList correctly prioritizes configured plugins over sandboxed ones with the same ID, and both over marketplace/registry rows. The enable/disable paths mirror the existing configured-plugin branch.
  • Type safety: EmDashHandlers now requires sandboxedPluginEntries. The middleware (both authenticated and anonymous paths), the MCP test helper, and runtime mocks are all updated. The anonymous fast path’s as EmDashHandlers cast is a pre-existing type hole and unchanged.
  • Tests: A handler-level unit test covers sandboxed surfacing and ID-shadowing (configured wins). A route-level integration test drives a real EmDashRuntime through the actual Astro routes for list/enable/disable.
  • Conventions: Changeset present, no raw SQL, ApiResult<T> envelope preserved, no admin UI strings to localize, no new indexes or content-table queries.
  • Edge cases: Leftover marketplace state rows for sandboxed IDs will display source: "config" (hardcoded) rather than the stale DB source. This is actually preferable to the configured-plugin behavior, which surfaces the stale source and can incorrectly render an Uninstall button.

The PR is genuinely clean — no findings.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sandboxed plugins registered via astro.config.mjs sandboxed:[] are missing from the admin Plugins screen

2 participants