Repository navigation
Conversation
d089255 to
e110703
Compare
hidb4ai
left a comment
There was a problem hiding this comment.
Reviewed e11070394247e80e1e19e0bcea4536ad6cd7399a, focusing on the changes after the #2616 head (d4b0f1b0). Requesting changes for four reproduced failures in snapshot retention, failed-update recovery, upgrade timeouts, and plugin OpenAPI references. Details are inline.
| for snapshot in plugin.iterdir(): | ||
| if snapshot.resolve() in keep: | ||
| continue | ||
| try: | ||
| if snapshot.stat().st_mtime < older_than: | ||
| remove_snapshot(snapshot) |
There was a problem hiding this comment.
[P1] Preserve snapshots still used by another Server process
keep contains only the starting process's current snapshots, but this directory is shared across Server processes. I reproduced this with two real Servers sharing POWERCONTEXT_HOME: A loaded v1 with rescanning disabled; after deploying v2, starting B deleted A's v1 snapshot. A's previously successful handler reading a file under ctx.plugin_dir then returned HTTP 500, while B served v2 successfully. The same exposure applies to an old host or Worker still draining during a rolling update. Snapshot age and this process's current version do not establish that no other process is using it; retain snapshots across process lifetimes before deleting them. The scan-time collector also needs this guarantee because _activate() adds reused snapshots to _created.
| except Exception as error: | ||
| await self._abandon(plugin, manifest, prepared, digest, error) | ||
| if old_host is not None: | ||
| self._spawn(self._retire(old_host, old_snapshot)) |
There was a problem hiding this comment.
[P2] Keep the old plugin when the new declaration cannot register
A declaration can pass the child-process check and still fail _register(), for example when it changes a Source Definition schema without changing that definition's name/version. On a real Server, changing the ticket input's min_length from 1 to 2 produced StoredPayloadConflictError; this branch then unloaded the working plugin, cleared its version/family/routes, and made its OpenAPI endpoint return 404. Rejecting the conflicting definition is correct, but the promised failed-update behavior is to keep the old version serving and record last_check_failure. Complete the fallible registration checks before switching, or restore the previous manifest, registrations, binding, and host on failure.
| await host.start() | ||
| # Without upgrade the first process starts in the background, so a slow start delays no request | ||
| # path; an upgrade must finish before the version is used. | ||
| if manifest.upgrade or plugin.host is not None: | ||
| failure = await host.wait_ready(READY_TIMEOUT_SECONDS + manifest.spec.upgrade_timeout_seconds) | ||
| if failure is not None: | ||
| await host.close() |
There was a problem hiding this comment.
[P2] Apply the upgrade deadline to the host watchdog too
Increasing wait_ready()'s timeout does not extend PluginHost._watch(): it still terminates every not-ready host after READY_TIMEOUT_SECONDS (60 seconds), and the host sends HostReady only after upgrade() and setup(). I reproduced a reload with upgrade_timeout_seconds=90 and an upgrade that sleeps for 65 seconds: the host was killed with start_timeout at 60 seconds, the plugin reported last_check_failure="start_failed", and v1 kept serving. Thus valid upgrades longer than 60 seconds cannot activate even with the documented 600-second default. Make the watchdog honor the declared upgrade allowance as well.
| if route.body == "json": | ||
| schema = route.request_schema or {} | ||
| operation["requestBody"] = {"required": True, "content": {"application/json": {"schema": schema}}} |
There was a problem hiding this comment.
[P2] Rebase nested model references in the plugin OpenAPI document
Pydantic schemas containing nested models use references such as #/$defs/Child. Embedding that schema here leaves the references relative to the OpenAPI document root, while $defs remains inside the individual request/response schema. I reproduced a plugin with Envelope(child: Child): its POST route returned 200, but the returned openapi.json contained two unresolved #/$defs/Child references, one for the request and one for the response. Consumers cannot resolve these otherwise valid models. Relocate definitions and rewrite references, or otherwise give each embedded schema a valid reference base; the response-schema insertion above needs the same treatment.
e110703 to
ffd85ec
Compare
… 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>
…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>
… 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>
ffd85ec to
f7a41a6
Compare
Which issue or RFC does this PR close?
Implements RFC #2579 (plugin hot reload, data upgrades, plugin route descriptions and MCP tools). Builds on #2616: this branch contains its commits, so review the commits after
d4b0f1b0and merge after #2616.Rationale for this change
#2616 loads plugins only at Server startup, so updating a plugin needs a restart, and plugins have no hook to adapt their data to a new version. RFC #2579 lets a running Server follow the plugin directory, runs each version's
upgradebefore it is used, and makes plugin routes discoverable through OpenAPI and MCP.What changes are included in this PR?
POWERCONTEXT_SERVER_PLUGINS_RESCAN_SECONDS(default 60,0off). A directory whose content digest is the same in two consecutive scans and differs from the version in use is copied to a read-only snapshot under the data directory, checked there, and added, updated, or removed without a restart. Plugin code is imported only from snapshots.ArtifactPlugin(upgrade=..., upgrade_timeout_seconds=600): called every time a version loads, beforesetup, as the plugin itself (reads and writer without a caller, at most one write transaction). Instances without host processes (thebackgroundrole) run it in a short-lived child before using the new version. A failed check or upgrade keeps the previous version and is reported inlast_check_failure.GET /v1/plugins/{name}/openapi.jsondescribes a plugin's routes. Routes declared withmcp=Trueare MCP tools named<plugin>__<handler>, listed from the plugins loaded at eachtools/listand called through the route itself./v1/pluginsaddsversion_digest,declaration_digest,activated_at,last_check_failure,routes, andpages.plugin.changedevents withplugin_name,plugin_event,version_digest,declaration_digest;plugin.scannedwithscan_ms.Are there any user-facing changes?
Yes. Plugin directory changes take effect without a restart; set
POWERCONTEXT_SERVER_PLUGINS_RESCAN_SECONDS=0to keep startup-only loading. New APIGET /v1/plugins/{name}/openapi.jsonand new fields on/v1/plugins; the Desktop contract is regenerated and qualified. No persisted format changes;release/upgrade-support.jsonrecords the review of the in-process authorization profile change.How was this change tested?
d4b0f1b0(same code, earlier hashes):uv run pytest -n 4— 10373 passed, 995 skipped, 36 failed. 32 failures were Kimi Code, ZCode, and installation tests that need Node, which the VM lacked; with Node installed they pass (74 passed). The other 4 were the upgrade release gate, fixed in this PR (tests/test_upgrade_release_gate.py: 132 passed).make docs-teston the VM: verified 1109 public pages and their internal links.uv run prek run) anduv run ty checkpass locally.sqlite-plugin-reload-5e365242-v1: anonymous loopback, static Bearer, HTTPS with CA/base path, and enforced Provider modes passed against the wheel built from5e365242;bundled_product_qualification_covers_every_desktop_operationpassed.tests/e2e/test_plugin_reload.pyruns a real Server with a 0.5 s rescan: a version change takes effect without restart and itsupgraderuns first; a version whoseupgradefails is not used and the previous version keeps serving;openapi.json; MCP lists and calls the plugin tool; an added plugin loads and a removed one disappears with its routes and tools.make js-test,make opencode-test, andmake cindy-testcompare them).background-role upgrade child was checked directly: a succeeding upgrade lets the version be used, a raising one does not.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