0.8.0 migration notes
Application authoring replaces plugin classes
Section titled “Application authoring replaces plugin classes”The public plugin, action, stream and describe_plugin imports have been
removed. Replace a decorated plugin class with app = QgisSdkPlugin(id=..., title=...)
and turn each action or stream method into a module function registered with
@app.action(...) or @app.stream(...). Give each function a first positional
ctx: PluginContext argument and use ctx rather than self.ctx. Actions
and streams need explicit IDs and titles; plugin metadata can default from the
module, but explicit stable IDs are recommended. Register surfaces with
@app.surface or app.add_surface(...) and providers with app.add_provider(...);
compose other QgisSdkPlugin objects using app.include(child). Change the manifest and bridge generation target from
module:Class to module:app, and use describe(app) or app.describe().
PluginRuntime, DesktopLifecycleAdapter, create_class_factory and
describe_bindings now take an application rather than a plugin class;
FakeUi(scope, app=app) checks its registered surfaces in pure tests. The
class-specific PluginFactory, instance_factory and declared_providers
APIs have been removed. For per-host context construction, assign
app.context_factory. The old class-constructor hooks have no direct replacement;
construct testable dependencies in the context factory or in ordinary functions.
The plugin HTTP API, and MCP for development only
Section titled “The plugin HTTP API, and MCP for development only”This release adds one runtime interface for clients outside QGIS and keeps MCP
strictly on the development side. No released version shipped the experimental
qgis_sdk.agent module, so there is nothing to migrate: it is not part of this
release, and there is no [tool.qgis-sdk.agent] metadata, AGENT check rule,
dev --agent mode or consent surface.
Calling a plugin from outside QGIS. A host starts the listener itself,
after runtime.start() and on the runtime’s owner thread:
from qgis_sdk.http import PluginHttpServer
server = PluginHttpServer( runtime, post=post_to_owner_thread, token_file=private_directory / "plugin.token", actions=("buffer", "watch_progress"), # an allowlist, not a view filter)server.start()# ... serve ...server.close()actions must name IDs the plugin actually declared; an unknown ID is refused
at construction. Nothing is exposed that the host does not name, construction
opens no socket and creates no token, and only one server may serve a runtime.
- New names:
qgis_sdk.http.PluginHttpServer,QgisSdkHttpError;qgis_sdk_tools.rest.PluginClient,PluginClientError; and the developer commandsqgis-sdk plugin describe|call|stream. - Endpoints:
GET /plugin(the plugin description filtered to the allowlist),POST /actions/<id>(a JSON object in, the action’s return value out), andPOST /streams/<id>(server-sent events, ending with a terminal frame). - Calls are serialized per runtime. A second call waits for its turn for up to
the server’s deadline and then fails with
503and thehost_busycode; it never runs inside another handler, including one that opened a modal dialog. Work queued from the thread already running plugin work is refused at once. A reload fails an in-flight call withhost_reloadedwithout undoing effects it already had. - Closing withdraws queued work, and the callers it withdrew answer
503withhost_reloadedbefore the connections are shut down:close()waits for the handlers already running, for up toclose_graceseconds (2 by default). A handler still running after that grace is cut off, so a close never hangs on a stuck action. - Refusals carry
{"error": {"code": ..., "message": ...}}.host_busy,host_reloadedand the per-plugin sessioncapacitybudget answer503, because waiting or retrying can succeed; malformed input and failed operations answer400.PluginClientraisesPluginClientErrorcarrying the status for any non-200reply, refuses a response larger than 1 MiB instead of parsing a truncated body, and reports a200action reply without avalueasPluginClientErrorrather than leaking aKeyError. PluginHttpServer.pending_requestsandPluginHttpServer.busyreport the dispatch state without exposing the internal queue: how many requests wait for the owner thread and whether one is running right now.- Every dispatch still runs on the QGIS owner thread and through the same
dispatcher, authority and protocol version as the page and the console. The
bridge,
BRIDGE_PROTOCOL_VERSIONandHOST_FINGERPRINTdo not move.
Security model. The listener binds to 127.0.0.1 only, refuses a foreign
Host header and any browser Origin, and requires a bearer token that the
host writes to a mode-0600 file, unlinks on close, and never puts on a
command line the plugin controls. Connections, request size, read and write
deadlines, event-stream slots and buffered bytes are all bounded. The allowlist
is a permission list, not a sandbox: an exposed action runs with the plugin’s
own authority, and the token holder can call anything the host named.
Support boundary: POSIX only. Creating a private token file relies on POSIX
permissions, and Windows credential storage is not implemented. On a non-POSIX
platform PluginHttpServer.start() refuses with a QgisSdkHttpError naming the
limitation, and creates no token or listener, rather than starting one whose
token another local account could read. This is a deliberate scope decision for
this release, not a transient bug; every other SDK feature, including the CLI
MCP server, works on Windows.
qgis-sdk mcp. pip install 'qgis-sdk[mcp]' adds the official mcp
package as an optional extra, and qgis-sdk mcp serves the developer CLI
commands over stdio. The dependency is imported only when that command runs; a
missing extra exits with the install line. This server never connects to a
running plugin, and the plugin REST commands are deliberately absent from its
inventory: use qgis-sdk plugin or PluginClient for that.
Externals with a probe, and pure-Python vendoring
Section titled “Externals with a probe, and pure-Python vendoring”[tool.qgis-sdk.build] gains two keys (decision-50):
externalaccepts tables beside bare roots:{ import = "pandas", requirement = "pandas>=2", reason = "reads spreadsheets" }.requirementdefaults to the import root;reasonis the sentence the startup probe shows. The declared roots move the matchingDEP001finding into theexternalbucket, appear inmetadata.txtasRequires: pandas (pandas>=2) - ..., and the generatedclassFactory- forbuildanddevalike - imports each one before anything of the plugin, explains a missing package in one message-bar error with the install command for the host, and returnsNoneso QGIS leaves the plugin unloaded. Nothing is installed on the user’s machine.vendorlists installed pure-Python distributions the build copies into<plugin>/_vendor/, with imports of those roots rewritten (in the plugin and inside the vendored code), licences under_vendor/LICENSES/and name/version in_vendor/NOTICE. A compiled distribution isDEP003; a requirement that is neither vendored nor external isDEP004.
New names: qgis_sdk_tools.project.BuildConfig and ExternalPackage
(ProjectConfig.build, external_roots), qgis_sdk_tools.distributions
(Distribution, read_distribution, normalize, file_path,
QGisSdkDistributionError), qgis_sdk_tools.check.buckets (BucketReport,
ImportEntry, bucket_imports, import_findings, the BUCKETS constants),
CheckReport.imports, BuildResult.vendored, and check --imports. An
import of a declared vendored root is now vendored instead of unknown, so
a project that vendored by hand and declared nothing keeps its DEP001
until it declares the distribution.
Processing destinations
Section titled “Processing destinations”Algorithms.run no longer forwards arbitrary named destinations directly to
Processing. Use an omitted destination for an SDK-owned output, or provide one
new local .geojson or .gpkg vector destination. Existing files (even with an
overwrite-enabled FileRef), multiple named destinations, memory/remote URIs,
raster, folder and other named outputs are refused. For intentional vector
replacement, run into an allocated output and use Layers.export_vector with
explicit overwrite consent. Other formats need a separately qualified
publication workflow. See Processing output safety for the
provider trust and cleanup limitations.