Skip to content

feat(sources): collect with Source plugins through the Server's runner and device runners - #2639

Open
jasper-yl wants to merge 38 commits into
oceanbase:masterfrom
jasper-yl:feat/source-plugins-impl
Open

jasper-yl wants to merge 38 commits into
oceanbase:masterfrom
jasper-yl:feat/source-plugins-impl

Conversation

@jasper-yl

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Implements RFC #2617 (Source plugins: collection runners and targets). Builds on #2638 (RFC #2579), which builds on #2616 (RFC #2549): this branch contains their commits, so review the commits after e1107039 and merge after #2638.

Rationale for this change

The previous Source plugin framework kept collection targets on each device behind a management service with a reverse channel. RFC #2617 keeps targets on the central Server, lets runners pull them, and makes Source plugins ordinary directory plugins next to Artifact plugins.

What changes are included in this PR?

  • Declaration. SourcePlugin(version, min_compatible_version, definitions, collect, target, scopes, setup, api, pages, upgrade) loads from POWERCONTEXT_SERVER_PLUGINS_DIR like Artifact plugins, including reloading and upgrade. A plugin with only definitions extends the Source write API; target declares a parameter model and identity parameters with fixed normalizers. Artifact and Source plugin declarations share PluginSpec.
  • Persistence. Revision v1_5_0_006 (SQLite and MySQL) adds pc_source_runners and pc_source_collect_targets and drops pc_source_write_plugins; its domain step refuses the upgrade while the old registry still lists plugins. Target checkpoints reuse pc_connector_checkpoints. Portable bundles add source_runner (name and bound subject) and source_collect_target records; runner reports and target status restart empty.
  • API and MCP. GET /v1/source-runners (pc_source_runners), POST /v1/source-runners/{runner}/sync, DELETE /v1/source-runners/{runner}, and /v1/scopes/{scope_id}/source-targets (pc_source_add, pc_source_status, pc_source_target_set_enabled, pc_source_target_delete). Targets of the server runner need server.admin; a runner name is bound to the subject of its first sync; identity parameters cannot change. POST /v1/source-observations and POST /v1/connector-checkpoints/commit accept the runner's plugin name and version (and the commit a target status); the Server checks the version against its plugin's compatible range (plugin_version_incompatible), and its plugin resolves and projects the observation. Device calls to plugin routes carry X-PowerContext-Plugin-Version.
  • Server runner. Each collecting Source plugin gets a Supervisor binding; a Worker collects one Scope's enabled server targets and commits the Sources, checkpoints, and target status in one transaction. CollectResult.fail fails one target while the others commit. New targets are collected right away.
  • Device runner. powercontext source-agent --server ... --plugins ... syncs every 30 seconds, collects on each plugin's schedule and new targets right away, submits Sources and then each checkpoint with its status, and retries on the next sync after plugin_version_incompatible. ctx.center.call_plugin reaches the plugin's own routes on the Server.
  • Removed. /v1/source-management/*, powercontext source load, the SOURCE_PLUGINS_DIR and SOURCE_MANAGEMENT_* settings, and powercontext.source_management. The tickets, ticket-chat, and images examples are rewritten as Source plugins; integration tool lists and the DSH secret scan follow the new MCP tools.
  • Docs (en and zh): the Source plugin guide is rewritten for runners and targets, with a migration section; the plugin writing guide and UDS deployment notes point to it.

Are there any user-facing changes?

Yes, breaking for the previous Source plugin framework: its API, CLI command, settings, and MCP tools (pc_source_capabilities, pc_source_instances, pc_source_instance_set_enabled) are removed, and the upgrade is refused while write plugins are still loaded. New API and MCP tools as listed above; the Desktop contract is regenerated and qualified. release/upgrade-support.json records the schema review.

How was this change tested?

  • Full suite on a Linux VM (Ubuntu 24.04, 4 cores, Node installed) at c7fb4b8c: uv run pytest -n 4 — 10378 passed, 984 skipped, 10 failed. All 10 are fixed in d057191d and pass on the VM (44 passed with the plugin e2e tests): the CLI imported server dependencies through source-agent, the OceanBase restore layer docs lacked the new tables, and two upgrade-history tests listed the domain steps without source_collection.
  • make docs-test: verified 1106 public pages and their internal links. make js-test, make opencode-test, and make cindy-test pass, with committed bundles current.
  • Hooks (uv run prek run) and uv run ty check pass locally; tests/test_upgrade_release_gate.py passes with the recorded schema review.
  • Desktop qualification sqlite-source-plugins-a80648a8-v1: anonymous loopback, static Bearer, HTTPS with CA/base path, and enforced Provider modes passed against the wheel built from a80648a8; bundled_product_qualification_covers_every_desktop_operation passed.
  • tests/e2e/test_source_plugins.py runs a real Server with an identity map and a real powercontext source-agent subprocess: only server.admin adds server targets; duplicate and conflicting targets; invalid parameters; collection on add; a failing target keeps its checkpoint while another target in the call commits; identity parameters are immutable; the device runner submits Sources and checkpoints and resubmits nothing next time; another credential cannot sync as the runner; a newer device plugin is rejected with plugin_version_incompatible; disabled targets are not synced; deleting keeps collected Sources.
  • Migration: v1_5_0_006 is refused while pc_source_write_plugins holds a row and otherwise adds the new tables; registered SQLite fingerprints of earlier revisions are unchanged; the MySQL layout resources pass the shared layout validation in the suite but were not run against a native OceanBase or seekDB.
  • tests/builtin/portability/test_bundle.py round-trips targets and their runner without status.

AI usage statement

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

🤖 Generated with Claude Code

@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.

Reviewed the Source runner/target changes after e110703 at d057191. The runner-ownership bypass, an accepted target configuration that prevents Scope export, and the split checkpoint/status transaction need to be fixed before merging. Details and reproductions are inline.

Comment on lines +4060 to +4063
if request.plugin is not None:
target = await _runner_target(http_request, request)
elif request.status is not None:
raise InvalidBaseAccessRequestError("status", "is recorded only for a runner's collection target")

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 target ownership independently of optional plugin metadata

Omitting both plugin and status skips _runner_target and sends the same target binding through the generic commit path. Against a real Server in enforced access mode, a different Scope contributor gets 403 with plugin present but 200 without it; the owning runner then syncs the injected checkpoint. This also lets that contributor change a reserved server target's checkpoint, although PATCHing that target correctly returns 403. A caller can therefore skip collection or poison its binding without runner/admin authority. Resolve collection-target bindings server-side and enforce their ownership/version checks regardless of which optional fields the caller supplies.

Comment on lines +2164 to +2166
elif record.record_type == "source_collect_target":
if payload["runner"] != "server" and not contains("source_runner", {"runner": payload["runner"]}):
raise BundleFormatError("collection target references a missing runner")

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] Keep accepted unsynced-runner targets exportable

create_source_target accepts a device runner that has never synced (the new e2e test does this too), so a persisted target can legitimately have no pc_source_runners row. This dependency check then rejects the entire Scope export. Reproduced by creating a ticket-chat target for never-synced through HTTP (201), then calling PortableBundleService.export: it raises BundleFormatError: collection target references a missing runner. A newly configured/offline device thus prevents backing up unrelated data in the same Scope. Make target admission and bundle dependencies agree so every accepted configuration can be exported and restored.

Comment on lines 4064 to +4069
result = await application.ingestion.commit(mapping.commit_connector_checkpoint_request(request))
if target is not None and request.status is not None:
from powercontext.builtin.persistence.source_collection import SourceCollectionRepository

status = request.status
async with _require_plugin_manager(http_request).database.transaction() as connection:

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] Commit the checkpoint and target status in one transaction

application.ingestion.commit durably commits the checkpoint before this separate status transaction starts, violating RFC #2617's atomic checkpoint/status contract. With a real SQLite Server and a trigger that rejects only the status UPDATE, the request returns 500 but the next sync already contains the new checkpoint ({"cursor":42}), while all target status fields remain untouched. Removing the trigger and retrying with the original expected checkpoint returns 409, so the failed request cannot recover its status write. Keep the checkpoint CAS and target status update in the same transaction so they commit or roll back together.

@jasper-yl
jasper-yl force-pushed the feat/source-plugins-impl branch from d057191 to faa5d6b Compare November 14, 2026 08:34
@jasper-yl
jasper-yl requested a review from frf12 as a code owner November 14, 2026 08:34
jasper-yl and others added 19 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>
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>
…, and expose routes as OpenAPI and MCP tools

- Each Server process rescans the plugin directory every POWERCONTEXT_SERVER_PLUGINS_RESCAN_SECONDS
  (default 60, 0 off). A directory whose content is stable for two scans is copied to a read-only
  snapshot, checked, and added, updated, or removed without a restart. Code is imported only from
  snapshots; a new version's host process takes requests once ready while the old one drains, and
  Workers launch from the version current at dispatch.
- `upgrade(ctx)` runs every time a version loads, before setup, as the plugin itself; instances without
  host processes run it in a short-lived child. A failed check or upgrade keeps the old version and is
  reported as `last_check_failure`.
- GET /v1/plugins/{name}/openapi.json describes a plugin's routes; routes with `mcp=True` are MCP tools
  named <plugin>__<handler>, listed from the plugins loaded now.
- /v1/plugins reports version and declaration digests, activation time, routes, and pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jasper-yl and others added 19 commits November 14, 2026 09:17
…support

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read-only directories made the data directory impossible to remove with ordinary tools;
snapshot files stay read-only.

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

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

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

get_plugin_openapi is authorized for any authenticated caller, as the Access
API is, but returns only a plugin's route descriptions and no Scope content.

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

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>
… description operation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r and device runners

Implements RFC oceanbase#2617 and replaces the previous Source plugin framework.

- `SourcePlugin` (version, min_compatible_version, definitions, collect, target, setup,
  api, pages, upgrade) loads from the plugin directory alongside Artifact plugins. A
  plugin with only definitions extends the Source write API; one with collect runs
  through runners.
- Revision v1_5_0_006 adds pc_source_runners and pc_source_collect_targets and drops
  pc_source_write_plugins, refusing the upgrade while it still lists plugins. Portable
  bundles carry runners (name and bound subject) and targets without runner status.
- Target API and MCP tools: pc_source_runners, pc_source_add, pc_source_status,
  pc_source_target_set_enabled, pc_source_target_delete; runners sync through
  POST /v1/source-runners/{runner}/sync. Targets of the server runner need
  server.admin; identity parameters are declared, normalized, and immutable.
- The server runner collects in Supervisor Workers and commits Sources, checkpoints,
  and target status in one transaction; CollectResult.fail reports one target.
- `powercontext source-agent` collects on devices: it submits Sources and then
  checkpoints with status. Runner requests carry the plugin version, accepted within
  the Server plugin's compatible range; the Server's plugin resolves and projects them.
- Removed: /v1/source-management/*, `powercontext source load`, the source management
  settings, and the old examples, which are rewritten as Source plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…document the new restore tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unner deletion in the access audit

The Source collection contract removed the old source management operations
and added delete_source_runner, which returns no Scope content.

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

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

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>
…on operation 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/source-plugins-impl branch from faa5d6b to 737484d 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.

2 participants