deer-flow/docs/full-stack-plugins.md
Wenchao An e2f19d8335
feat(plugins): full-stack plugin APIs and bookmarks (#5647)
* feat(plugins): add full-stack contributions and bookmarks example

* ci(plugins): provision bookmark gateway for browser tests

* fix(plugins): authenticate module downloads through configured backend

* fix(plugins): isolate contributions and localize extension UI

* fix(plugins): preserve bookmark agent routing and contain async callbacks

* fix(plugins): pin durable batch workers to app extension snapshots
2026-09-22 11:18:57 +08:00

10 KiB

Full-stack plugin contributions

A deployment-installed Python extension can register a PluginContribution with optional browser code, authenticated backend actions and model tools. This extends the existing install(registry, config) workflow. MCP and Skills keep their existing APIs and lifecycles. Public contracts live in deerflow_extension_api (0.2.2).

The browser contribution API in this slice is experimental. BrowserModule(code=...) is an MVP transport for validating page/action host interfaces, not the final asset packaging contract or a requirement that all future plugins ship one JavaScript file.

What users see

Capability Center has an Extensions tab with read-only information and deployment status. A plugin can add a conversation action, its own workspace page and an optional sidebar entry. The bookmarks example uses all three: save the last visible answer, then search, rename or delete it under My bookmarks. Existing notification and Markdown/JSON export behavior is unchanged.

Registration and execution

registry.plugin(...) returns True when accepted. Its default public protocol implementation returns False on a host without support, so packages must check the result. A declaration needs a unique namespace and at least one browser module, backend action or tool. Validation happens before registration; install failures use the existing positional rollback and source attribution.

  • BrowserModule(module, code, public_fields=()) contains a self-contained ES module, at most 512 KiB. It may export surfaces and conversationActions with apiVersion: 1.
  • BackendAction(name, handler) declares an async handler receiving a JSON object and ActionContext(principal, settings). The principal comes from host authentication.
  • ModelTool(name, description, input_schema, handler, group="extensions") declares an async handler with a ToolContext that additionally carries the thread ID. Object schemas must be inline: reference resolution is rejected. Tools enter the ordinary host tool assembly, including group filtering and later authorization.
  • SettingsField describes a non-secret deployment value. There is no online override store or settings write API. Browser clients see only enabled and explicitly listed public fields. Plugin code remains responsible for business authorization.

Tool names are namespace-derived and collision-checked. Tool inputs are bounded to 256 KiB, outputs to 64 KiB and execution to 30 seconds. Backend actions accept object inputs up to 256 KiB and have a 30-second timeout. Cancellation does not guarantee rollback of external effects or already-running worker-thread operations.

Durable batch_task workers pin the Gateway app's extension snapshot at startup and use it for both plugin tools and subagent execution. Recovered items use the new worker's snapshot after restart; no Python snapshot is stored in the durable execution_spec. Standalone batch services without an explicit snapshot capture the process default once at construction.

Browser API

The authenticated host exposes:

Route Purpose
GET /api/plugins Deployed descriptors, public configuration and declared actions
GET /api/plugins/modules/{module}/{sha256}.mjs Installed code with a content revision
POST /api/plugins/{namespace}/actions/{name} Invoke one declared backend action

The host does not accept filesystem paths, import strings or arbitrary remote URLs from the browser. Asset responses use JavaScript content type, nosniff and private no-store caching. Existing Gateway session/CSRF policies apply; PATs do not gain a new route allowlist. The browser sends its descriptor's viewer ID so the action route can reject a stale view after account changes, in addition to normal request authentication.

Module downloads honor NEXT_PUBLIC_BACKEND_BASE_URL, including a path prefix, and use the host's authenticated fetch helper before importing a temporary Blob URL. The URL is released after import, including on failure. Browser modules must be self-contained: relative imports and assets resolved against import.meta.url are unsupported. Deployments with a Content Security Policy must allow blob: in script-src and the configured backend in connect-src; split-origin backends must allow credentialed CORS from the frontend, as for other host API calls.

A page surface declares id, slot: "page", title, mount(root, context) and optional navigation: { label, labelZh?, icon? }. The host generates the URL /workspace/extensions/{namespace}/{id} and mounts only that registered page. mount returns a synchronous dispose() callback. The context includes locale, public settings, an abort signal and a namespace-bound callBackend helper. The optional openConversation(threadId) host helper reads current conversation metadata through the authenticated API and uses the host's normal/custom-agent routing rules. Missing or inaccessible conversations reject without navigating; unmount/account changes abort pending reads and prevent late navigation. Cleanup aborts outstanding work and fences late callbacks. Plugin async work should observe the signal and release resources in dispose().

Conversation actions receive a conversation context and host services. The latestVisibleAnswer and conversationText services reuse the existing export sanitizer; sidebar reads go through the authenticated conversation API. Plugin code must still escape user/model text when rendering it. Factories and availability callbacks must be synchronous; invalid Promise returns are rejected and their rejections consumed. The host validates each locale-dependent action group and evaluates availability inside a per-plugin error boundary. A malformed or throwing contribution is omitted without removing healthy plugin actions or failing the conversation page. Plugin tool names that collide with ordinary tools follow the host's ordinary-first deduplication; unrelated tools remain available. Duplicate names within the plugin tool set still fail strict validation.

Packaged assets and compatibility direction

RFC #5510 proposes a manifest plus packaged static resources (ui_manifest.json and static/dist/...). That remains the intended direction for larger plugins. The current single-file transport cannot naturally support relative chunks, separate CSS, images, fonts, WASM, source maps or import.meta.url assets, and holds the module as a Python string. Its no-store response intentionally provides no immutable cache reuse.

A follow-up should add a distinct, versioned packaged-asset declaration alongside the inline form, rather than silently changing the meaning of BrowserModule.code:

  • A validated manifest identifies the entry module and permitted files under a package-owned asset root. The root comes from the installed package, never a browser supplied filesystem path.
  • Namespace/revision-scoped URLs, for example /api/plugins/{namespace}/assets/{revision}/{path}, must confine canonical paths to that root, reject traversal and escaping symlinks, and serve only manifest-listed files with correct MIME types and nosniff.
  • Revisioned assets should support immutable caching. Private assets must retain authentication and private-cache policy; public/CDN caching needs an explicit public distribution contract. Cache invalidation and removal semantics must be specified.
  • Entry modules and relative dependencies must share an authenticated loading design for both same-origin and split-origin deployments. The current Blob importer cannot simply be reused for relative chunks; an authenticated same-origin asset proxy is one option to evaluate.
  • Discovery should negotiate the supported transport/version and reject unsupported transports clearly. Existing inline v1 packages should keep working while the new transport reuses the namespace, page/action interfaces and deployment lifecycle.

These are compatibility requirements for the follow-up, not implemented asset APIs. The stable packaging contract requires review before plugin authors rely on it. Neither transport should require rebuilding DeerFlow's frontend for each compatible plugin.

Trust and lifecycle

Browser and Python plugins are trusted operator-installed code. Browser modules run in the main page. Shadow DOM scopes CSS; it is not a security sandbox, and a plugin can access same-origin browser capabilities. No shared React instance is promised: plugins mount their own DOM rather than providing a component for the host React tree. Sandboxed iframes, right-side panels, composer selection and version negotiation beyond API v1 checks are follow-up designs discussed in #5510 and #5539.

Install, remove, enable/disable or upgrade with the existing deployment/CLI workflow and restart the service. Python packages must be delivered into the actual execution environment. Once this host contract is installed, a new compatible plugin does not require plugin-specific host frontend compilation. Container delivery still needs a persistent package installation or an image containing the package.

Browsers retain their discovered plugin set until manual refresh; there is no automatic refresh or hot-unload guarantee. Old UI does not guarantee old backend code is retained across a service restart. A changed asset revision or removed action fails explicitly and requires reload. Disabled/unloaded pages have no navigation entry after refresh; visiting an unavailable page directly does not mount a plugin.

Validation scope

The bookmark E2E test runs the production frontend against real Python action handlers and SQLite, with synthetic authentication and scripted LangGraph ToolNode calls. It covers persistence, owner isolation, read-only deployment management and the full page workflow. It does not claim live model behavior or a full production deployment. The example uses single-host storage, not a multi-node persistence contract.