Expand plugin host surface (#5205)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - The plugin system is the extension boundary for optional product
capabilities
> - Rich plugins need more than a worker entrypoint: they need scoped
database storage, local project folders, managed agents/routines, host
navigation, and reusable UI components
> - The LLM Wiki work exposed those missing host surfaces while keeping
plugin code outside the core control plane
> - This pull request expands the core plugin host, SDK, server APIs,
and UI bridge so plugins can declare and use those surfaces
> - The benefit is that future plugins can integrate with Paperclip
through documented, validated contracts instead of bespoke server or UI
imports

## What Changed

- Added plugin-managed database namespaces and migration tracking,
including Drizzle schema/migration files and SQL validation for
namespace isolation.
- Added server support for plugin local folders, managed agents, managed
routines, scoped plugin APIs, and plugin operation visibility.
- Expanded shared plugin manifest/types/validators and SDK
host/testing/UI exports for richer plugin surfaces.
- Added reusable UI pieces for file trees, managed routines, resizable
sidebars, route sidebars, and plugin bridge initialization.
- Updated plugin docs and example plugins to use the expanded host and
SDK surface.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
packages/shared/src/validators/plugin.test.ts
server/src/__tests__/plugin-database.test.ts
server/src/__tests__/plugin-local-folders.test.ts
server/src/__tests__/plugin-managed-agents.test.ts
server/src/__tests__/plugin-managed-routines.test.ts
server/src/__tests__/plugin-orchestration-apis.test.ts
ui/src/api/plugins.test.ts ui/src/components/FileTree.test.tsx
ui/src/components/ResizableSidebarPane.test.tsx
ui/src/pages/PluginPage.test.tsx ui/src/plugins/bridge.test.ts` passed:
11 files, 67 tests.
- Confirmed this PR changes 89 files and does not include
`pnpm-lock.yaml` or `.github/workflows/*`.

## Risks

- Medium: this expands plugin host contracts across db/shared/server/ui
and includes a new core migration (`0076_useful_elektra.sql`).
- The plugin database namespace validator is intentionally restrictive;
plugin authors may need follow-up affordances for SQL patterns that
remain blocked.
- Merge this before the LLM Wiki plugin PR so the plugin can resolve the
new SDK and host APIs.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5 coding agent, tool-enabled shell/git/GitHub
workflow. Context window size was not exposed by the runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-05-05 07:42:57 -05:00 committed by GitHub
parent d6bee62f02
commit 3c73ed26b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 27516 additions and 914 deletions

View file

@ -1,6 +1,6 @@
// @vitest-environment node
import type { ReactNode } from "react";
import type { ComponentProps, ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
@ -33,7 +33,11 @@ vi.mock("../api/issues", () => ({
issuesApi: mockIssuesApi,
}));
function renderMarkdown(children: string, seededIssues: Array<{ identifier: string; status: string; title?: string }> = []) {
function renderMarkdown(
children: string,
seededIssues: Array<{ identifier: string; status: string; title?: string }> = [],
props: Partial<ComponentProps<typeof MarkdownBody>> = {},
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@ -54,7 +58,7 @@ function renderMarkdown(children: string, seededIssues: Array<{ identifier: stri
return renderToStaticMarkup(
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<MarkdownBody>{children}</MarkdownBody>
<MarkdownBody {...props}>{children}</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
@ -279,6 +283,64 @@ describe("MarkdownBody", () => {
expect(html).toContain('href="PAP-1271"');
});
it("leaves wiki links as text unless explicitly enabled", () => {
const html = renderMarkdown("See [[wiki/entities/paperclip]].");
expect(html).toContain("[[wiki/entities/paperclip]]");
expect(html).not.toContain('href="/wiki/page/wiki/entities/paperclip.md"');
});
it("renders wiki links with a custom resolver when enabled", () => {
const html = renderMarkdown(
"See [[wiki/entities/paperclip|Paperclip]] and [[wiki/entities/dotta-b]].",
[],
{
enableWikiLinks: true,
resolveWikiLinkHref: (target) => `/wiki/page/${target.endsWith(".md") ? target : `${target}.md`}`,
},
);
expect(html).toContain('href="/wiki/page/wiki/entities/paperclip.md"');
expect(html).toContain('data-paperclip-wiki-link="true"');
expect(html).toContain('data-paperclip-wiki-target="wiki/entities/paperclip"');
expect(html).toContain(">Paperclip</a>");
expect(html).toContain('href="/wiki/page/wiki/entities/dotta-b.md"');
expect(html).toContain(">wiki/entities/dotta-b</a>");
expect(html).not.toContain("[[wiki/entities/paperclip");
});
it("keeps wiki links as text when the custom resolver rejects them", () => {
const html = renderMarkdown(
"See [[wiki/entities/paperclip]].",
[],
{
enableWikiLinks: true,
wikiLinkRoot: "/wiki/page",
resolveWikiLinkHref: () => null,
},
);
expect(html).toContain("[[wiki/entities/paperclip]]");
expect(html).not.toContain('data-paperclip-wiki-link="true"');
expect(html).not.toContain('href="/wiki/page/wiki/entities/paperclip"');
});
it("does not render wiki links inside code spans or code blocks", () => {
const html = renderMarkdown(
"Inline `[[wiki/entities/paperclip]]`.\n\n```md\n[[wiki/entities/dotta-b]]\n```",
[],
{
enableWikiLinks: true,
wikiLinkRoot: "/wiki/page",
},
);
expect(html).toContain("[[wiki/entities/paperclip]]");
expect(html).toContain("[[wiki/entities/dotta-b]]");
expect(html).not.toContain('href="/wiki/page/wiki/entities/paperclip"');
expect(html).not.toContain('href="/wiki/page/wiki/entities/dotta-b"');
});
it("applies wrap-friendly styles to long inline content", () => {
const html = renderMarkdown("averyveryveryveryveryveryveryveryveryverylongtoken");