{"record":{"id":"52438a76007f393f","repo":"paperclipai/paperclip","slug":"plugin-does-not-declare-a-ui-bundle","errorCode":null,"errorMessage":"Plugin does not declare a UI bundle","messagePattern":"Plugin does not declare a UI bundle","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":278,"sourceCode":"      plugin = await registry.getByKey(pluginId);\n    }\n\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n\n    // Step 2: Verify the plugin is ready and has UI declared\n    if (plugin.status !== \"ready\") {\n      res.status(403).json({\n        error: `Plugin UI is not available (status: ${plugin.status})`,\n      });\n      return;\n    }\n\n    const manifest = plugin.manifestJson;\n    if (!manifest?.entrypoints?.ui) {\n      res.status(404).json({ error: \"Plugin does not declare a UI bundle\" });\n      return;\n    }\n\n    const rawCompanyId = req.query.companyId;\n    if (\n      Array.isArray(rawCompanyId) ||\n      (rawCompanyId !== undefined && typeof rawCompanyId !== \"string\")\n    ) {\n      throw badRequest('\"companyId\" must be a string when provided');\n    }\n    const companyId = typeof rawCompanyId === \"string\" ? rawCompanyId.trim() : \"\";\n    if (companyId) {\n      assertCompanyAccess(req, companyId);\n    }\n\n    // Step 2b: Check for devUiUrl in company-scoped plugin config — proxy to\n    // local dev server when a plugin author has configured hot-reload.\n    // See PLUGIN_SPEC.md §27.2 — Local Development Workflow","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L260-L296","documentation":"Returned as HTTP 404 by GET /_plugins/:pluginId/ui/* (server/src/routes/plugin-ui-static.ts:278) when the plugin exists and is 'ready', but its stored manifest has no entrypoints.ui field. Per PLUGIN_SPEC §19.0.3 the host only serves bundles for plugins that declare a UI entrypoint; without it there is nothing on disk to serve.","triggerScenarios":"GET /_plugins/<pluginId>/ui/<any file> for a plugin whose manifest.json lacks entrypoints.ui — e.g. a tools-only or backend-only plugin, or a plugin version published before it gained a UI. Also hit when the extension-slot renderer unconditionally mounts UI for every installed plugin instead of filtering for UI-capable ones.","commonSituations":"Plugin authors who never declared entrypoints.ui in their manifest; a re-published plugin whose manifest was regenerated without the ui entrypoint; host UI code that assumes all plugins ship a UI bundle; using the wrong pluginId (a sibling plugin that has no UI).","solutions":["Fetch the plugin record and confirm manifestJson.entrypoints.ui exists before requesting /_plugins/:id/ui/* assets","If the plugin should have a UI, add entrypoints.ui (e.g. \"./dist/ui/\") to its manifest, rebuild, and reinstall the plugin","In the host UI, only render extension slots for plugins whose manifest declares entrypoints.ui","Verify you are using the correct pluginId/pluginKey — a sibling plugin without UI returns this same 404"],"exampleFix":"// before\nconst mod = await import(`/_plugins/${pluginId}/ui/${entry}`);\n\n// after\nconst plugin = await api.getPlugin(pluginId);\nif (!plugin.manifestJson?.entrypoints?.ui) {\n  // Plugin has no UI bundle — skip mounting the extension slot\n  return null;\n}\nconst mod = await import(`/_plugins/${pluginId}/ui/${entry}`);","handlingStrategy":"validation","validationCode":"const plugin = await api.getPlugin(pluginId);\nconst hasUi = Boolean(plugin.manifestJson?.entrypoints?.ui);\nif (!hasUi) return null; // skip mounting this plugin's UI","typeGuard":"type UiCapablePlugin = { manifestJson: { entrypoints: { ui: string } } };\nconst hasUiBundle = (p: unknown): p is UiCapablePlugin =>\n  Boolean((p as { manifestJson?: { entrypoints?: { ui?: string } } })\n    ?.manifestJson?.entrypoints?.ui);","tryCatchPattern":null,"preventionTips":["Only render UI extension slots for plugins whose manifest declares entrypoints.ui","Plugin authors: run a post-build check that dist/ui exists and the manifest references it before publishing"],"tags":["plugins","plugin-ui","manifest","http-404"],"backgroundTag":"manifest-missing-field","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}