Files
LangBot/en/plugin/certified-plugins.mdx
T
Hyu 85fb49c687 docs(plugin): refocus the certification page on developer decisions
Rewrite plugin/certified-plugins in all three locales around the
questions a plugin developer actually asks, and make it the single home
for certification: what it is, why it exists, when it is needed, what
changes after approval, and what the contract requires.

- Decision framing restored, including a needs/does-not-need table and
  the explicit statement that most plugins do not need certification.
- Full manifest example, accepted values for both declaration fields and
  the SDK version that recognises them.
- Constraints kept and tightened: stateless rules, allowed/forbidden
  table, anti-patterns with correct/incorrect snippets, Runner notes,
  self-check list.
- Submission flow moved onto this page so it no longer depends on the
  publishing page.
- Operator content that duplicates the Core architecture document
  removed (Cloud admission matrix, OSS issuer keys, certificate fields,
  internal review tiers).

publish/market is now publishing-only with a one-line pointer, and its
misplaced plugin_submission screenshot moves back under the upload
section where it belongs. dev/basic-info replaces the commented-out
manifest fields with a link to the contract page.

ja had been missing whole sections (36 lines); all three locales now
share the same section structure.
2026-09-29 14:40:58 +08:00

190 lines
10 KiB
Plaintext

---
title: "Certified plugins and shared runtime"
description: "What certification is, why it exists, when you need it, and the shared-runtime-v1 / stateless-v1 contract."
---
## What certification is
Certification is a grant LangBot makes for **one exact, immutable plugin artifact**: that artifact is approved to run **shared across tenants** in LangBot Cloud.
- Certification is bound to the artifact itself (a specific published version, identified by digest), not to a plugin name or an author. Publishing a new version does not carry the previous version's certification over.
- Certification is not a quality score and not a security audit endorsement.
## Why certification exists
LangBot does not place plugins in shared mode by default. The reason is that sharing is keyed by artifact:
- **Dedicated mode**: the plugin does not participate in sharing and runs on its own.
- **Shared mode**: every installation of the same artifact digest shares one Worker.
Sharing keeps the number of processes and the startup cost from growing linearly with the number of installations. The trade-off is that **one process handles calls from several Workspaces at once** — so a plugin that keeps tenant data on its instances will leak it between tenants.
The platform therefore has to review this exact artifact, confirm it satisfies the stateless contract, and then pin that trust to the artifact with a signature before it may enter shared placement. That is what certification is for.
## When you need certification
**Most plugins do not need certification.** Skipping it does not stop you from publishing to the marketplace, and it does not stop users from installing your plugin — it simply runs in dedicated mode.
| Your situation | Certification needed? |
| --- | --- |
| You only want to publish the plugin so users can install it | No |
| The plugin needs process-local state across calls (for example a long-lived external session process) | No — and it should stay dedicated |
| The plugin has background tasks, timers, or long-lived tenant caches | No — dedicated mode is the safer choice |
| You want the Certified Plugin badge | Yes |
| You want several installations of one artifact to share a process, reducing cost and startup overhead | Yes |
| The plugin targets Cloud multi-tenant scenarios and you want shared placement | Yes |
The test is a single question: **certification only makes sense if your code genuinely keeps no tenant or installation data on its instances.**
## What certification changes
After review passes:
- The artifact is signed and installed in **shared mode**; multiple installations of the same digest share one Worker.
- That Worker holds one `BasePlugin` object and one instance per declared component manifest (EventListener, Tool, Command, KnowledgeEngine, Parser, Page, Runner).
- Each installation adds only a lightweight layer of configuration, authority and lifecycle context; the active one is selected per call.
- You receive the Certified Plugin badge.
- Self-hosted OSS ships without a certification issuer public key, so a certified artifact still runs dedicated on self-hosted instances.
You also take on a standing obligation: **every later version must keep satisfying the stateless contract.** After changing code you submit a new version for review; certification does not renew itself.
## What you must adapt
### Declaration
Declare both fields under `execution` in `manifest.yaml`:
```yaml
apiVersion: v1
kind: Plugin
metadata:
author: your-name
name: YourPlugin
version: 0.1.1
spec:
config: []
components: {}
execution:
python:
path: main.py
attr: YourPlugin
sharedRuntime: shared-runtime-v1
componentModel: stateless-v1
```
| Field | Accepted value | Notes |
| --- | --- | --- |
| `execution.sharedRuntime` | `shared-runtime-v1` | This is the only accepted value; anything else fails manifest validation |
| `execution.componentModel` | `stateless-v1` | This is the only accepted value |
- Both fields **must be declared together**. A package that declares `sharedRuntime` without `componentModel` is rejected during certification validation.
- The fields are recognised from **SDK 0.7.0** onwards. Earlier SDKs ignore them, so do not rely on an old SDK to tell you whether the declaration took effect.
<Warning>
A declaration is an application, not approval. It grants no badge and does not change how the plugin runs by itself.
</Warning>
### Stateless constraints
- `BasePlugin.initialize()` and component `initialize()` are **process-scoped** and run once per Worker. At that point there is no "current installation" and no "current Workspace".
- Component instances are called concurrently by different Workspaces, so every method must be re-entrant.
- Read the active installation configuration per call: `self.get_config()` on `BasePlugin`, `self.get_plugin_config()` or `self.plugin.get_config()` in a component, and `ctx.config` in a Runner.
- Never store configuration, credentials, Workspace or installation identity, or request data in instance fields or module-level globals.
- Do not read and cache installation configuration in `initialize()`.
- Reach storage, files, credentials, logs, models and other platform capabilities through the SDK Host APIs. Authority is resolved per call; do not cache it yourself.
- Durable tenant state belongs in Host services. Runners use `ctx.state_get()` / `ctx.state_set()` and friends (this requires `ctx.context.available_apis.state` to be `true`). In-process caches may hold artifact-level public data only; if you must cache tenant data, the key has to include the complete installation identity and must be cleaned up when the installation is removed.
- Tenant-bearing background tasks and threads are forbidden. Once a handler returns, that invocation's context and run-associated calls are no longer valid.
### Allowed / forbidden
| Concern | Allowed | Forbidden |
| --- | --- | --- |
| Reading config | Read per call (`self.get_plugin_config()`, `ctx.config`) | Reading in `initialize()` and keeping it on `self` |
| Connection pools / HTTP clients | Shared pools without tenant credentials | Baking an installation's endpoint or API key into the instance |
| Caching | Artifact-level public data; or keys including the full installation identity and supporting cleanup | A module-level `dict` keyed by Workspace or user that persists |
| Cross-invocation state | Host services (`ctx.state_*` in a Runner) | Instance fields, module globals |
| Background work | None | Holding an invocation's context or its run-associated calls after the handler returned |
| Logging | `ctx.log()` or SDK logging APIs | A module-level background thread printing tenant config or credentials |
### Common anti-patterns
**1. Reading installation config during initialization**
```python
async def initialize(self):
self.endpoint = self.plugin.config["endpoint"] # process-scoped init has no active installation
self.api_key = self.plugin.config["api_key"]
```
Shared initialization has no installation configuration, so this reads empty or stale values.
**2. Caching tenant data in a module global**
```python
_CACHE = {} # keyed by Workspace or user; persists across installations and tenants
async def call(self, params, **kwargs):
if params["user"] not in _CACHE:
_CACHE[params["user"]] = await fetch(params["user"])
```
**3. Keeping work alive after the handler returns**
```python
async def call(self, params, **kwargs):
config = self.get_plugin_config()
asyncio.create_task(self._upload_later(config)) # handler already returned; context and authority are gone
return ok()
```
**4. Treating instance fields as a config container**
```python
async def initialize(self):
self.default_endpoint = "https://api.example.com" # a public constant is fine
self.last_config = {} # writing installation config here is not
```
The correct shape:
```python
class LookupTool(Tool):
async def initialize(self):
# artifact-level objects only, no tenant information
self.transport = StatelessHTTPTransport()
async def call(self, params, **kwargs):
config = self.get_plugin_config() # read for the active installation on every call
return await self.transport.lookup(config["endpoint"], params)
```
### Runners in particular
Runners have the most tension with the stateless contract, because a runner naturally tends to keep sessions and context in the process.
- A `RunnerContext` is **valid only for the current run**. Do not keep it, or anything derived from it, on the instance for reuse across calls.
- Session state a Runner needs across calls belongs in `ctx.state_get()` / `ctx.state_set()` (scopes: `conversation`, `actor`, `subject`, `runner`) or another Host-provided store — not in instance fields.
- After a handler returns, do not keep using `ctx` in background tasks, and do not keep using run-associated calls made through `self.plugin`.
- If your Runner fundamentally depends on process-local state across calls (for example a long-lived external session process), **do not declare `stateless-v1`** and stay on a dedicated Worker.
See [Runner context](/en/plugin/dev/apis/agent-run) for the full context API.
## Self-check and submission
Verify each item before submitting:
- [ ] Both fields are declared in `manifest.yaml` with the exact accepted values.
- [ ] A repository-wide search confirms there are no module-level mutable globals.
- [ ] `initialize()` performs process-scoped setup only and reads no installation config.
- [ ] Every piece of installation config is read at call time.
- [ ] No task or thread keeps running after a handler returns.
- [ ] Cross-call state goes through Host APIs (`ctx.state_*` in a Runner).
### How to submit
Certification uses the same path as publishing, with no extra process: declare the two fields above in `manifest.yaml`, then submit a **new version** with `lbp publish` as usual (a published version cannot have the declaration added later). LangBot Space checks the exact submitted package, runs an advisory code review, and issues the Certified Plugin badge after human approval; certification is bound to the reviewed version and checksum.
If the review finds that the release cannot meet the requirements, publish it as an unsigned normal plugin first and fix the findings in a later immutable version.
If this check shows your plugin is not a good fit for sharing, just publish it as a normal plugin — certification is optional. See [Publish to the marketplace](/en/plugin/dev/publish/market) for the publishing flow.