SDK 2 implements plugin contract 2 and drops contract 1. There is no compatibility layer: SPOT 2.0 calls only contract-2 plugins, and a 1.x plugin must be ported before it runs on SPOT 2.0.
Pin spot-sdk-python==2.0.0, and add uvicorn to serve the app.
Describe the plugin with a Manifest: contract=2, its kind, its egress and its settings.
Replace the settings class with a PluginSettings subclass. Secret settings now come from files in /run/secrets, the token included.
Replace the FastAPI app and its routes with handlers given to create_plugin_app. It serves /health, /manifest and the /internal/* routes, and checks the plugin's token.
Analyzers read an EmailView from AnalyzeRequest.email and answer an AnalyzerVerdict: a class, a 0-100 score, a confidence and signals.
Build the image with the labels of Manifest.labels().
Test with view_from_eml, make_view and assert_contract.
IngestionResult.verdict, score, and the new email_id
Workflow.response_mode
none: the mode of a job comes with it (IngestionResult.mode, RespondRequest.mode), from its connection and its route; a retriever applies its actions only in enforce, and tells SPOT what it did with SpotClient.report_action
PluginKind, FailureStrategy and IngestionStatus are StrEnums: str(PluginKind.ANALYZER) is "analyzer".
EmailView is serialised with by_alias=True: its from_ field is from in JSON.
PluginSettings reads a dotenv file only when the subclass sets env_file; BaseAnalyzerSettings read .env.
A secret file in /run/secrets wins over an environment variable of the same name.
KnowledgeClient.delete(doc_id) deletes one of the plugin's own documents, at DELETE /sources/{source}/documents/{doc_id}, both parts quoted, so ids holding / or # reach the store whole. It needs the plugin's id (SPOT_PLUGIN_ID, or source=): without one it raises ValueError, as replace_source does. A document's id is unique within its source, not across the store.
The clients use httpx2 instead of httpx. A plugin that gives a client a transport= (httpx2.MockTransport in its tests), writes a handler of httpx2.Request and httpx2.Response, or catches an error around a client call imports httpx2, and adds httpx2 to its dependencies where it uses it directly. The clients let httpx2's errors through (httpx2.HTTPError, httpx2.TransportError, httpx2.TimeoutException, httpx2.ConnectError, and httpx2.HTTPStatusError from LLMClient): an except httpx.TransportError around a client call catches nothing, and becomes except httpx2.TransportError. httpx2 logs each request, URL included, under the httpx2 logger (and httpcore2.*), which configure_logging sets to WARNING: a plugin that set the level of the httpx logger to keep URLs out of its logs needs no setting of its own. httpx2 verifies certificates against the operating system's trust store rather than certifi's bundle: an image needs the ca-certificates package, and an internal CA installed in the system store is trusted.
The package requires FastAPI 0.133 or later and Starlette 1.7 or later: a plugin moves to them, which assert_contract and assert_provider_contract also need, as their TestClient uses httpx2.
configure_logging writes JSON lines by default (LOG_FORMAT=json), or readable lines with LOG_FORMAT=text; simple and detailed raise ValueError at start. A JSON line writes the extra= fields, which 1.x dropped: give none that holds an email's content or a secret. Call configure_logging() at import, at the top of the module uvicorn loads the app from, not in a lifespan: uvicorn writes its first lines before the lifespan runs, and they stay uvicorn's text otherwise.
A plugin has exactly one kind: Manifest(kinds=[PluginKind.ANALYZER]) becomes Manifest(kind=PluginKind.ANALYZER), the spot.plugin.kind label holds that one value, and GET /manifest answers kind. A manifest given kinds is refused. A plugin of several kinds becomes one plugin per kind, each with its own id and image; SPOT core 2.0 lists only images whose label names one kind.