Skip to content

feat(plugins): load directory plugins that generate, present, search, and change their own Artifacts - #2616

Open
jasper-yl wants to merge 20 commits into
oceanbase:masterfrom
jasper-yl:feat/plugin-interfaces-impl
Open

jasper-yl wants to merge 20 commits into
oceanbase:masterfrom
jasper-yl:feat/plugin-interfaces-impl

Conversation

@jasper-yl

@jasper-yl jasper-yl commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Implements RFC #2549 (plugin interfaces) and the plugin APIs and Dashboard pages of RFC #2579 (plugin hot reload, APIs, and pages), and replaces the managed Artifact plugin framework added in #2246. Hot reload, plugin data upgrades, and MCP tools from RFC #2579 are follow-ups; standalone Source plugins and collection are proposed in RFC #2617.

Rationale for this change

RFC #2549 replaces plugin frameworks that make PC Server manage plugin lifecycle with a few interfaces PC Server calls when it needs them. RFC #2579 lets plugins add their own operations and pages, which domain plugins such as document knowledge bases need. Keeping the #2246 framework alongside would mean two plugin models, two sets of admin APIs, and readiness checks that reject the new plugins' bindings, so this PR removes it in the same change.

What changes are included in this PR?

A plugin is a directory with plugin.py under POWERCONTEXT_SERVER_PLUGINS_DIR; plugin.py assigns an ArtifactPlugin declaration to plugin.

  • Loading (builtin/plugins/manifest.py, server/plugins.py): Server imports each plugin in a time-limited child process, validates the declaration, records a content fingerprint, and registers the family, the plugin's own Source types, and a Supervisor binding when the plugin has a schedule or on_sources. A plugin that fails to load, or whose setup result lacks a required method, is failed in GET /v1/plugins without affecting Server.
  • Generation (builtin/plugins/executor.py, writer.py): generate(request, writer) runs in Supervisor Workers; writes in async with writer.transaction() commit together with the Source cursor. generate is required only with a schedule or on_sources.
  • Presentation and search: render_artifact (required) serves GET /v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/rendering. Optional search_artifacts receives the complete generic search request; core keeps only current, active revisions the caller may read, in the plugin's order. POST /v1/context/prepare sections may list plugin families, with an optional per-section search passed through. Core builds no index for plugin families.
  • User changes (builtin/plugins/host.py, server/plugin_hosts.py): generic create, replace, merge, forget, and restoration on plugin families run the plugin's methods in a per-plugin host process that authorizes as the caller, checks revisions, and records lineage-only provenance. Unimplemented methods return 422.
  • Plugin Source types: plugins may declare Source Definitions, written through POST /v1/scopes/{scope_id}/sources with source_type or by plugin routes with ctx.sources.submit.
  • Plugin APIs and pages: declared routes mount at /v1/plugins/{name}/api{path} under POWERCONTEXT_SERVER_PLUGIN_API_MAX_BODY_BYTES (default 32 MiB); static pages run in a sandboxed Dashboard frame that forwards only the plugin's own routes with the session.
  • Host processes (server/plugin_hosts.py): each plugin has its own group of host processes. One always runs; another starts while every process is busy and requests queue, up to the declared host_processes (default 1, overridable per plugin with POWERCONTEXT_SERVER_PLUGIN_HOST_PROCESSES, capped for all plugins with POWERCONTEXT_SERVER_PLUGIN_TOTAL_HOST_PROCESSES); extra processes stop after 5 idle minutes. A call past its time limit is cancelled in the host and returns 504 plugin_timeout while other calls continue; a host whose heartbeat stops for 5 seconds is terminated, its other calls return 503, and it restarts after a backoff. Prepared Context leaves out a plugin section whose plugin is unavailable and records the failed search in the receipt. Cron without a time zone uses the machine's.
  • Resources: ctx.storage is a dedicated SQLite file per plugin; ctx.models exposes the Server's embedding and generation models without their credentials.
  • Removal of the feat(artifact): add managed plugin framework and some example #2246 framework: /v1/admin/artifact-plugins/*, powercontext.artifact_plugins, declarative family schemas and indexes, custom family searchers, the POWERCONTEXT_SERVER_ARTIFACT_PLUGINS_DIR setting, and its examples and tests.
  • Persistence: revision v1_5_0_005 (SQLite and MySQL) drops the feat(artifact): add managed plugin framework and some example #2246 tables and registration columns only when they hold no data (the domain preflight otherwise refuses with unsupported_plugin_data), adds nullable pc_artifacts.metadata with the plugin name and code digest of plugin-written revisions, and accepts plugin: bindings in its own readiness step. Frozen revision resources are unchanged. Revision reads omit metadata so domain steps at earlier revisions still run. Portable artifact_revision records move to schema version 3 and still restore version 2.
  • Integration bundles: the DSH, Cindy, and OpenCode bundles are rebuilt from the regenerated operation tables.
  • Docs: a plugin writing guide at docs/{en,zh}/docs/develop/plugins.md; the OceanBase restore table lists no longer name removed tables.

A complete knowledge base (RAG) example plugin built on this branch is at https://github.com/jasper-yl/powercontext/tree/feat/knowledge-base-plugin-v2/examples/plugins/knowledge_base and is not part of this PR.

Are there any user-facing changes?

Yes.

  • Breaking: the feat(artifact): add managed plugin framework and some example #2246 admin APIs, powercontext.artifact_plugins, and POWERCONTEXT_SERVER_ARTIFACT_PLUGINS_DIR are removed. Upgrading a database whose feat(artifact): add managed plugin framework and some example #2246 plugins still hold data is refused until the deployer exports and removes them on the previous version. ArtifactRecord.schema_version, ArtifactCollectionItem.schema_version, and Capabilities.custom_artifact_schemas are removed.
  • New: POWERCONTEXT_SERVER_PLUGINS_DIR, POWERCONTEXT_SERVER_PLUGIN_API_MAX_BODY_BYTES; GET /v1/plugins, GET /v1/plugins/{name}, GET /v1/plugins/{name}/scopes; the rendering endpoint; governance, merge, forget, and restoration routes for every registered family; plugin routes and Dashboard plugin pages.
  • Typed clients: CreateArtifactRequest becomes a choice between CreateBuiltinArtifactRequest and CreatePluginArtifactRequest; ArtifactCreated.family, CreateSourceRequest.source_type, TaggableArtifactFamily, ContextAssemblySection.family, and PreparedContextFamily become strings; ContextAssemblyFamily is removed. Wire formats of existing requests are unchanged.
  • release/upgrade-support.json records the persistence review for v1_5_0_005.
  • The Desktop contract is regenerated and qualified (sqlite-plugin-interfaces-e0501451-v1).

How was this change tested?

  • Full suite on a Linux VM (Ubuntu 24.04, 4 cores) at e0501451: uv run pytest -n 4 — 10387 passed, 1013 skipped, 1 failed: test_bundled_desktop_contract_has_a_qualified_business_profile, which the Desktop qualification commit in this PR resolves (tests/test_desktop_contract.py then passes locally, 4 passed).

  • make docs-test on the same VM: verified 1108 public pages and their internal links.

  • make check hooks (uv run prek run --all-files) and uv run ty check pass locally.

  • Desktop qualification sqlite-plugin-interfaces-e0501451-v1 (desktop/tests/real_server.py, product mode): anonymous loopback, static Bearer, HTTPS with CA/base path, and enforced Provider modes passed against the wheel built from e0501451; bundled_product_qualification_covers_every_desktop_operation passed.

  • Migration: tests/builtin/persistence/test_plugin_storage_migration.py upgrades a SQLite database at v1_5_0_004, is refused while pc_artifact_family_schemas holds a row, and after the row is removed drops the old tables and columns, adds pc_artifacts.metadata, and keeps existing data; the registered SQLite fingerprints of every earlier revision are unchanged. The MySQL resources for v1_5_0_005 pass the shared layout validation in the suite; they were not run against a native OceanBase or seekDB.

  • Plugin end-to-end tests run a real Server process: tests/e2e/test_plugins.py (loading, generation, rendering, passthrough search with an unsupported field rejected, Prepared Context with a plugin section, user changes, tags, merges) and tests/e2e/test_plugin_pages.py.

  • The knowledge base example on the fork branch passes its end-to-end test against this branch, including rendering, generic search with mode, and Prepared Context with a per-section search.

  • tests/e2e/test_plugin_hosts.py runs a real Server: a timed-out call returns 504 while another on the same host completes; Prepared Context with a timing-out plugin section still returns and its receipt records the failed search; blocking the event loop gives the timed-out call 504 and the other 503, then a new host serves; sixteen long calls start a second host. Hooks and ty check pass locally, and the related plugin and Prepared Context tests pass (123).

AI usage statement

Implemented with Claude Code (Claude Opus 5.5) under the author's direction; the author reviewed the design decisions. Claude Code ran the tests and manual checks listed above.

🤖 Generated with Claude Code

@Zxf-xufeng Zxf-xufeng left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Static review of the plugin host/writer implementation at c408880f. I found two concurrency issues to address. I have not run the test suite.

[P1] Preserve request preconditions through the actual write transaction

In builtin/plugins/host.py, _change() checks the requested revision/governance version using _precheck(), then creates a separate writer for the plugin call (lines 345–388). UserChange retains the operation and IDs but not those expected versions. In writer.py, set_lifecycle() and merge() reread the current records and build their commit plans from those newer records.

For example, a forget request for revision 1 can pass its precheck, another request can publish revision 2, and the original forget can then deprecate revision 2 successfully instead of rejecting the stale request. The documented forget_artifact implementation follows this path. Merge inputs have the same gap: the versions used by the transaction can differ from those supplied by the caller.

Please carry the caller's expected revisions and governance versions into the writer and enforce them inside the write transaction. The framework should enforce the preconditions independently of whether the plugin repeats the checks. Add a regression test that pauses after the initial precheck, commits a competing update, and verifies that the stale forget/merge fails without changing the newer state.

[P2] Return the revision committed by this invocation, not a subsequent head

After the plugin commits, _change() reads the latest artifact again and returns that snapshot (host.py, lines 388–398). If request A commits revision 2 and request B commits revision 3 before A's final read, A can receive B's content/revision as the result of its own operation. Its response can also combine that later snapshot with A's lifecycle outcome.

Please retain the exact committed snapshot/result in the writer and use it for the response. Add an interleaving test that commits a second update between the first commit and response construction; each response should identify its own committed revision.

References: host.py, writer.py.

jasper-yl added a commit to jasper-yl/powercontext that referenced this pull request Nov 13, 2026
…he interfaces each plugin implements

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@hidb4ai hidb4ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes for two reproduced correctness failures: plugin API writers can bypass Artifact write authorization, and supported v2 Artifact archives cannot be restored. The generated integration contracts also need to be synchronized. The existing concurrency findings are not duplicated here.

Comment on lines +581 to +585
owner = self._owner
await owner.artifacts.lock_heads(
self._connection,
owner.scope_id,
((await self._require(artifact_id)).artifact.as_ref(),),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Enforce Artifact write authorization for direct writer mutations

A plugin route with access="scope.contribute" can obtain ctx.writer(scope_id) and modify another user's Artifact through set_tags() or the direct set_lifecycle(..., "retired") branch at lines 478–487. These paths write through repositories without calling the shared Artifact authorization service; _require() also reads with a None context. Reproduced with a real Server, spawned plugin host, and the built-in multi-Bearer identities: Alice creates a note; Bob has only scope.contributor. Bob's generic tag/content updates and plugin tx.put() return 403, but plugin tx.set_tags() and tx.set_lifecycle(..., "retired") both return 200 and persist the changes. Please enforce the caller's artifact.write permission inside the transaction for every direct mutation, including tags and lifecycle transitions, with a regression covering a non-owner contributor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reproduced with the same setup (owner plus a scope.contributor acting through a plugin route) and fixed in de8c50a: set_tags, bind_handle, and the direct lifecycle transition now run the shared artifact.write check before writing; Worker writers are unchanged. tests/e2e/test_plugin_access.py uses a multi-Bearer identity map: a non-owner contributor gets 403 for tag, retire, and bind through a plugin route (204 before the fix), and the owner succeeds.

Comment on lines 1459 to 1460
payload = {name: payload.get(name) for name in spec.payload}
record = _Record(record_type=typed, identity=identity, payload=payload, digest=digest)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Preserve legacy record identity through staging and restore

This normalizes a v2 payload by adding metadata=None but retains its v2 digest. _record_document() subsequently emits that record with schema version 3, so the restore staging index contains a document whose digest no longer matches its version/payload. A three-record archive exported by base 1cb0e75d restores successfully on that base; on this head, validate() reports compatible=True, then restore() fails with BundleFormatError: record digest does not match content and inserts nothing. The same logical revision already present with null metadata is also incorrectly classified as an immutable conflict because target digests use v3. Please retain the verified original identity or normalize it consistently across staging and target comparisons, and test actual v2 restore plus repeat/identical-target restore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reproduced: an archive exported by the released v1.5.1 failed restore with "record digest does not match content". Fixed in a9e1ff3: a verified schema-2 artifact_revision record is re-identified at the current schema for staging and target comparison, while the bundle total digest still covers the archived digests. The regression restores a real v1.5.1 archive (checked in as tests/fixtures/portable_archives/v1_5_1_profile_history.pcb), restores it again (0 inserted, 14 already present), and re-exports and restores the current-schema archive into the same target. The archive docs now describe schema 3 and the accepted schema 2.

Comment thread openapi/powercontext.yaml
description: >-
Lists the plugins checked at Server startup, whether each loaded or failed, and what each one declares.
Plugins have no persisted lifecycle state; change the plugin directory and restart Server.
operationId: list_plugins

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Regenerate all shipped consumers of the changed API contract

The OpenAPI changes are only partially propagated. In CI run 43340872845, rebuilding DSH, OpenCode, and Cindy changes their committed bundles, Dify reports stale tool declarations/contract.json, and all four Desktop jobs fail export_ipc --check. I also reproduced the Dify generator failure on this head. For example, wire.rs removes the old plugin schema fields while desktop/ui/src/generated/ipc.ts still declares them; the shipped JS bundles retain the removed admin operations and omit the new ones. Please regenerate and commit the built bundles, Dify contracts, and Desktop IPC types together with the source contract so the shipped clients and contract checks agree.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This spans more than the OpenAPI document on c408880f: three shipped consumers still carry the removed endpoints.

  • e2e/bub/tests/test_access_audit.py:263-269 still lists list_artifact_plugins, get_artifact_plugin, enable_all_artifact_plugins, reload_artifact_plugin, enable_artifact_plugin, disable_artifact_plugin, unload_artifact_plugin in _SERVER_WIDE_WITHOUT_CONTENT, while openapi/powercontext.yaml has zero artifact-plugins matches. test_every_server_wide_operation_is_classified_by_whether_it_returns_scope_content therefore fails at line 295, and harness-validation / validate ends 2 failed, 304 passed on make harness-test (job 131643284529).
  • integrations/opencode/plugins/powercontext/lib/index.js:239-305 and integrations/dsh/plugins/powercontext/lib/index.js:339-405 still declare the seven /v1/admin/artifact-plugins… operations, while their src/operations.generated.ts no longer contain any of them. The committed bundles are stale relative to their own sources, so opencode-package and dsh-package fail on the bundle-consistency step (jobs 131643275397 and 131643275520).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Regenerated and committed (28e50ab, also covering @AlexStocks' points): the Dify tool declarations and contract.json, the Desktop IPC types, and the Bub access audit (the old *_artifact_plugin* operations removed; list_plugins, get_plugin, and list_plugin_scopes classified as returning no Scope content). The DSH, OpenCode, and Cindy bundles were rebuilt in d4b0f1b.

Regenerating Dify exposed a contract bug: adding the plugin branch (content: any object) to ReplaceArtifactRequest's oneOf made every built-in request match two branches, so JSON Schema validators such as the Dify tools rejected built-in edits. The create and replace bodies now use anyOf (effee7c), since the Server selects the branch by family name; the generated Python models are unchanged and the Desktop contract is requalified. I also relocked e2e/bub and evaluation for the new tzlocal dependency and aligned the Bub harness assembly check with the client omitting an unused per-section search.

At 8fe6162: make check, harness-test, dify-test, contract-test, the Desktop IPC check, the JS/OpenCode/Cindy builds, docs-test, and the full pytest suite pass.

scope_id = request.scope_id
if scope_id is None:
raise ValueError("rendering needs a Scope") # noqa: TRY003
await self.require("scope.read", scope_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] 读路径的 Scope 授权原本有 e2e 断言,本 PR 删除后替代套件没有恢复

触发条件:本 PR 之后任何对插件家族读授权的回归,即 host.py 里这几处 require("scope.read", ...) 的语义被改(_render 252 行、_search 275 行、_CheckedReads 531 行、_allowed 625 行)。

实际影响:本 PR 删掉了 tests/e2e/test_artifact_authorization_startup.py(-501 行),其中 test_custom_artifact_authorization_survives_server_restart 断言的正是这条不变量:

denied = await client.get(f"/v1/scopes/{scopes[1]}/artifacts/{family}/report")
assert denied.status_code == 403, denied.text

并且验证了重启后授权仍然生效。替代它的两个套件里没有任何授权断言:tests/e2e/test_plugins.py(3 个用例:插件加载与 on_sources 派发、通用合并路由、通用来源 API)与 tests/builtin/plugins/test_plugin_writer.py(5 个用例:写入游标、生命周期/合并、陈旧读冲突、用户变更、生命周期目标)对 contribute|forbidden|AccessDenied|denied|authoriz 的命中数都是 0。

于是"插件家族产物只对在该 Scope 有授权的调用方可见"这条安全性不变量,现在只剩 host 里的实现,没有任何端到端断言。本 PR 同一区域已经出现过一次同类的授权缺口(builtin/plugins/writer.py:585 的 P1:ctx.writer() 绕过 contribute 授权),说明这条边界确实会被改动穿过——而这次删除恰好移掉了能拦住它的测试。

证据:deleted hunk tests/e2e/test_artifact_authorization_startup.py 中的 assert denied.status_code == 403;两个新测试文件的关键字命中数均为 0(grep -ci 'contribute|forbidden|AccessDenied|denied|authoriz');src/powercontext/builtin/plugins/host.py 的四处授权点。

修复方向:在 tests/e2e/test_plugins.py 补一个用例——建两个 Scope,只对第一个授予 viewer,断言对第二个 Scope 的 /v1/scopes/{id}/artifacts/{family}/...(含搜索路由)返回 403,并覆盖重启后仍生效。这样删掉旧套件才是净等价替换,而不是覆盖面减少。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

同意,读路径的授权断言需要恢复(de8c50a86)。tests/e2e/test_plugin_access.py 新增的用例中,只在一个 Scope 有 viewer 权限的调用者访问另一个 Scope 的插件 Artifact、rendering、列表、search 以及 Prepared Context 的插件 section,都返回 403;在有权限的 Scope 中返回 200。同一个用例也覆盖了 hidb4ai 指出的写路径越权。

"重启后仍生效"这部分没有加:旧测试防的是 #2246 框架把插件授权配置持久化后、重启时丢失的问题;新框架每次请求都通过统一的访问服务授权,插件不保存任何授权状态,重启不会引入需要验证的新状态。如果你认为仍有必要,我再补上。

@jasper-yl

Copy link
Copy Markdown
Contributor Author

@Zxf-xufeng Thanks — both reproduced and fixed in 6966dd1.

[P1] Preconditions through the write transaction. UserChange now carries the caller's (revision, state_version) for each target, and the transaction checks them against the rows it actually writes (put, set_lifecycle, merge), returning 409 (412 for content updates) whether or not the plugin repeats the check. The host precheck stays as a fast path.

[P2] Return the committed revision. The writer keeps the snapshot and records its agreed write committed; the host returns them and the Server builds the replace, create, forget, and merge responses from them instead of rereading the head.

Regression: tests/e2e/test_plugin_concurrent_changes.py runs a real Server and plugin host. The plugin pauses at a gate after the precheck while the test commits a competing update through the generic API; the stale forget and merge return 409 and the newer state is unchanged (without the fix, the forget deprecated revision 2 as you described). A second gate pauses after an update commits while another update commits; each response reports its own revision and content.

jasper-yl and others added 10 commits November 14, 2026 09:09
… and change their own Artifacts

Implements the Artifact plugin part of RFC oceanbase#2549 and the plugin APIs and
Dashboard pages of RFC oceanbase#2579, and replaces the managed Artifact plugin
framework (oceanbase#2246).

- A plugin is a directory with plugin.py assigning an ArtifactPlugin. Server
  checks it in a child process; generate runs in Supervisor Workers, and
  render_artifact, search_artifacts, user changes, Source writes of the
  plugin's own types, and plugin routes run in a per-plugin host process.
- Plugins present (GET .../rendering) and search their own families: generic
  search passes the complete request to search_artifacts, and Prepared Context
  sections may list plugin families. Core builds no index for them.
- Revision v1_5_0_005 removes the previous framework's storage only when it is
  empty, adds pc_artifacts.metadata (plugin name and code digest), and accepts
  plugin: bindings; frozen revision resources are unchanged. Portable revision
  records move to schema version 3 and still restore version 2.
- Plugin APIs mount at /v1/plugins/{name}/api with a configurable body
  ceiling, and plugin pages run in a sandboxed Dashboard frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cancel only timed-out calls

- Each plugin keeps one host process and starts more while every process is full and
  requests queue, up to `host_processes` (overridable per plugin with
  POWERCONTEXT_SERVER_PLUGIN_HOST_PROCESSES, capped in total by
  POWERCONTEXT_SERVER_PLUGIN_TOTAL_HOST_PROCESSES); extra processes stop after 5 idle minutes.
- A call past its time limit is cancelled in the host and returns 504 `plugin_timeout`;
  other calls continue. A host whose heartbeat stops for 5 seconds is terminated, its other
  calls return 503, and it restarts after a backoff.
- Prepared Context leaves out a plugin section whose plugin is unavailable and records the
  failed search in the receipt.
- `Cron` without a time zone uses the machine's time zone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d lifecycle writes

Plugin route writers act as the caller, but set_tags, bind_handle, and the
direct lifecycle transition (retirement, deprecation with a replacement)
wrote through repositories without the shared Artifact access check, so a
Scope contributor could retag or retire another user's Artifact through a
plugin route. These writes now run the same artifact.write check as content
changes; Worker writers keep their unchanged security.

The new acceptance test also restores the read-side assertions removed with
the old plugin authorization suite: a viewer of one Scope gets 403 for plugin
Artifacts, renderings, search, and Prepared Context sections of another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and report the committed revision

The host checked expected revisions and governance versions before calling
the plugin, but the writer then decided forget, merge, and update against the
rows it reread, so a write committed after the precheck could be forgotten or
merged by a stale request. User changes now carry the caller's expectations
into the transaction, which rejects a changed target with 409 (412 for
updates) whether or not the plugin repeats the check.

Responses no longer reread the head after the plugin returns: the writer
keeps the snapshot and records its agreed write committed, and the Server
builds replace, create, forget, and merge responses from them, so a later
write cannot appear as the result of an earlier request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on plugin writes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema version 2 artifact_revision records were verified against their
archived digest and then staged as version 3 with that same digest, so
restoring any archive exported by v1.4.1 through v1.5.1 failed with "record
digest does not match content", and the restored revisions would have been
reported as immutable conflicts on a repeat restore. A verified earlier-schema
record is now re-identified at the current schema for staging and target
comparison, while the bundle total digest still covers the archived digests.

The regression test restores, re-restores, and re-exports an archive produced
by the released v1.5.1 runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it for the plugin API contract

The OpenAPI changes removed the managed plugin operations and custom family
schemas and opened Artifact families to loaded plugins, but the Dify tool
declarations and contract, the Desktop IPC types, and the Bub access audit
still described the old contract. Regenerate them, and classify the new
plugin listing, detail, and Scope progress operations as returning no Scope
content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin family's create and replace bodies accept any content object, so a
built-in family's request also matched the plugin branch and failed oneOf's
exactly-one rule in JSON Schema validators; the Dify tools rejected every
built-in Artifact edit as an invalid request. The Server selects the branch by
family name, which anyOf states accurately. Generated Python models keep the
same unions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct request bodies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jasper-yl and others added 10 commits November 14, 2026 09:09
The host process groups added tzlocal to the builtin extras without relocking
the projects that depend on powercontext by path, so their --locked checks
failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odies

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ness and fix new test typing

The Prepared Context client drops an unused per-section search so an older
Server accepts the request; the Bub compatibility check still compared the
payload with the full model dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n runtime and its Dashboard titles

Point plugin imports at powercontext.builtin.authz, pass the section to the
Prepared Context budget evaluation's section builder, and title plugin pages
through the Dashboard's document_title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e after rebasing onto master

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in their own section

Rebasing onto master's grouped navigation dropped the closing tag of the group
loop, so every Dashboard page with plugin pages failed to render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasper-yl
jasper-yl force-pushed the feat/plugin-interfaces-impl branch from 8fe6162 to 34ffdae Compare November 14, 2026 18:29

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants