Files
LangBot/ja/plugin/certified-plugins.mdx
T
Hyu e279f2fae8 docs(plugin): document per-installation reuse and installation revocation
A Workspace installs a plugin once, but that single installation is reused by
several product objects: every processor for a Runner, every knowledge base for
a KnowledgeEngine / Parser / Retriever, and many conversations or page requests
for the other component kinds. The platform storage key carries no processor or
knowledge-base dimension, so a plugin must fold that identity into its own keys.

- certified-plugins: add "one installation, many usages" with the isolation
  dimensions per component kind, and "when an installation is revoked"
  documenting BasePlugin.on_installation_revoked, its trigger set, the absent
  invocation context and the no-op/failure semantics.
- component pages (runner, knowledge-engine, knowledge-retriever, parser, tool,
  command, event-listener, page): add a short component-specific section with
  that component's own rules and a link to the contract page.
- Align the background-work rule with the SDK contract: the SDK does not track
  ownership of detached work, so a plugin must keep its own binding-keyed
  registry and stop it in the revocation hook.

All three locales carry the same sections.
2026-09-29 20:10:56 +08:00

228 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "認証プラグインと共有実行"
description: "認証とは何か、なぜ存在するのか、いつ必要か、そして shared-runtime-v1 / stateless-v1 の規約。"
---
## 認証とは
認証とは、LangBot が**正確で不変な 1 つのプラグイン artifact** に対して付与する資格です。その artifact は LangBot Cloud で**テナントをまたいで共有実行**することが承認されます。
- 認証は artifact そのもの(digest で識別される特定の公開バージョン)に紐付き、プラグイン名や作者には紐付きません。新しいバージョンを公開しても、前のバージョンの認証は引き継がれません。
- 認証は品質評価ではなく、セキュリティ監査の保証でもありません。
## なぜ認証が必要か
LangBot は既定ではプラグインを共有実行しません。共有の単位が artifact だからです。
- **専用モード**:共有に参加せず、単独で実行します。
- **共有モード**:同じ artifact digest のすべての installation が 1 つの Worker を共有します。
共有の利点は、プロセス数と起動コストが installation 数に比例して増えないことです。代償は、**1 つのプロセスが複数の Workspace からの呼び出しを同時に処理する**ことです。インスタンスにテナントデータを置いているプラグインは、テナント間でデータが混ざります。
そのためプラットフォームは、この artifact を審査してステートレス規約を満たすことを確認し、その信頼を署名で artifact に固定したうえで、共有配置に載せます。これが認証の目的です。
## 認証が必要な場合
**ほとんどのプラグインには認証は不要です。** 認証しなくてもマーケットプレイスへの公開は可能で、ユーザーの利用も妨げられません。プラグインは専用モードで動作します。
| 状況 | 認証の要否 |
| --- | --- |
| マーケットプレイスに公開してユーザーに使ってもらいたいだけ | 不要 |
| 呼び出しをまたぐプロセス内状態が必要(常駐する外部セッションプロセスなど) | 不要。専用実行のままにしてください |
| バックグラウンドタスク、タイマー、長期のテナントキャッシュがある | 不要。専用モードのほうが安全です |
| 認証プラグインの表示がほしい | 必要 |
| 同じ artifact の複数 installation で 1 つのプロセスを共有し、コストと起動負荷を下げたい | 必要 |
| Cloud のマルチテナント向けで、共有配置されたい | 必要 |
判断基準は 1 つだけです。**インスタンスにテナントや installation に紐づくデータを一切保存しないと本当に言い切れる場合にのみ、認証に意味があります。**
## 認証後の効果
審査を通過すると、次のようになります。
- artifact が署名され、インストール時は**共有モード**で実行されます。同じ digest の複数の installation が 1 つの Worker を共有します。
- その Worker 内には `BasePlugin` が 1 つ、宣言した各コンポーネント manifest につき 1 つのインスタンス(EventListener、Tool、Command、KnowledgeEngine、Parser、Page、Runner)だけが存在します。
- 各 installation は軽量な設定・権限・ライフサイクルコンテキストを 1 つだけ保持し、呼び出しごとに現在のリクエストがどれを使うかを決定します。
- 認証プラグインの表示が付与されます。
- セルフホスト OSS は認証発行者の公開鍵を既定で持たないため、認証済みの artifact でもセルフホスト環境では専用モードで動作します。
同時に継続的な義務も発生します。**以降のすべてのバージョンがステートレス規約を満たし続ける必要があります。** コードを変更したら新しいバージョンを提出して再審査を受けます。認証は自動更新されません。
## 必要な対応
### 宣言
`manifest.yaml` の `execution` 配下に 2 つのフィールドを同時に宣言します。
```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
```
| フィールド | 有効な値 | 説明 |
| --- | --- | --- |
| `execution.sharedRuntime` | `shared-runtime-v1` | この値のみ受け付けます。他の値を書くとマニフェスト検証に失敗します |
| `execution.componentModel` | `stateless-v1` | この値のみ受け付けます |
- 2 つのフィールドは**必ず同時に**宣言します。`sharedRuntime` だけを宣言し `componentModel` を書かないパッケージは、認証検証の段階で拒否されます。
- これらのフィールドは **SDK 0.7.0** 以降で認識されます。それ以前の SDK は無視するため、古い SDK で宣言の有効性を確認しないでください。
<Warning>
宣言は申請であり、承認ではありません。宣言だけでは認証表示も付与されず、実行モードも変わりません。
</Warning>
### ステートレス規約
- `BasePlugin.initialize()` とコンポーネントの `initialize()` は**プロセス単位**の初期化で、Worker ごとに 1 回だけ実行されます。この時点では「現在のインストール」も「現在の Workspace」も存在しません。
- コンポーネントインスタンスは異なる Workspace から同時に呼び出されるため、すべてのメソッドは再入可能でなければなりません。
- 現在のインストール設定は呼び出しごとに読み取ります。`BasePlugin` では `self.get_config()`、コンポーネントでは `self.get_plugin_config()` または `self.plugin.get_config()`、Runner では `ctx.config` を使用します。
- 設定、資格情報、Workspace / installation ID、リクエストデータをインスタンス属性やモジュールグローバルに保存しないでください。
- `initialize()` でインストール設定を読み取り、保持しないでください。
- ストレージ、ファイル、資格情報、ログ、モデルなどのプラットフォーム機能は SDK の Host API 経由で利用します。権限は呼び出しごとに解決されるため、自分でキャッシュしないでください。
- 呼び出しをまたいで保持する必要があるテナント状態は Host サービスに置きます。Runner は `ctx.state_get()` / `ctx.state_set()` などを使用します(`ctx.context.available_apis.state` が `true` である必要があります)。プロセス内キャッシュは artifact レベルの公開データのみ許可されます。テナントデータをキャッシュする場合は、キーに完全な installation 識別子を含め、インストール削除時に解放できるようにしてください。
- テナントデータを伴うバックグラウンドタスクやスレッドを開始しないでください。SDK は自分で作成した処理の所有権を**追跡しません**。どうしてもハンドラーから切り離す場合は、installation binding をキーにした登録表を保ち、`on_installation_revoked()` で停止してください。ハンドラーが戻った時点で、その呼び出しのコンテキストと実行関連の呼び出しは無効になります。
### 1 つの installation、複数の用途
1 つの Workspace でプラグインをインストールできるのは 1 回ですが、その installation は**複数のプロダクトオブジェクト**から再利用されます。
| コンポーネント種別 | 再利用するもの | 分離のされ方 |
| --- | --- | --- |
| Agent / パイプラインの Runner | 各プロセッサ | `ctx.state.*` は既にプロセッサ単位(スコープに `binding_identity`(Agent UUID 由来)を含む) |
| ナレッジベースの KnowledgeEngine / Parser | 各ナレッジベース | **プラグイン自身がナレッジベース ID で分離する必要があります** |
| Tool / Command / EventListener / Page | 複数のプロセッサ、会話、ページリクエスト | 呼び出しごとに読み取り、インスタンスに蓄積しない |
プラットフォームのストレージキーは `(instance, Workspace, owner type, plugin, key)` で、**プロセッサやナレッジベースの次元を含みません**。したがって永続化やキャッシュを行う場合は、キーにプロダクトオブジェクトの識別子を含めてください。
- **設定**:呼び出しごとに読み取る(`ctx.config`、または task-local な `self.get_plugin_config()`)。インスタンスにキャッシュしない。
- **永続化**:キーにプロダクトの識別子(プロセッサ ID / ナレッジベース ID / 会話 ID)を含める。
- **プロセス内キャッシュ**:キーに完全な installation binding **と** プロダクトの識別子の両方を含め、installation が取り消されたときに解放する(下記)。
- **ロックとフェンス**:`(installation, プロダクトオブジェクト)` 単位で記録する。**1 つのプロセッサやナレッジベースの失敗が、同じ installation の他を遮断してはいけません。**
- **失敗の切り分け**:手動対账が必要な状態に入るのは、送出済みで結果不明の変更だけにします。決定的な検証エラーや送出前のキャンセルは後続に影響させない。
- **ログとテレメトリ**:診断の失敗が業務に影響してはいけませんし、テナントデータをプロセス全体のシングルトンに書かないでください。
### installation が取り消されたとき
共有配置では 1 つのオブジェクトグラフが同じ artifact の全 installation にサービスするため、**1 つの installation を取り消してもオブジェクトグラフは破棄されません**。プラグインが binding 単位のプロセス内状態を自分で解放する必要があります。
```python
class MyPlugin(BasePlugin):
def __init__(self) -> None:
super().__init__()
self._caches: dict[str, Cache] = {}
async def on_installation_revoked(self, binding) -> None:
self._caches.pop(binding.installation_uuid, None)
```
- **呼ばれる場面**:アンインストール、無効化、Workspace の削除、および**アップグレード**(旧 binding が取り消される)。共有配置でのみ呼ばれます。専用ワーカーは停止されるため、プロセス内状態はプロセスとともに消えます。
- **呼び出し中は有効な invocation がありません**。Host API(ストレージ、設定、binding 参照)は利用できず、解放できるのはプロセス内状態だけです。installation は引数で渡されます。
- 例外はログに記録され、**取り消し処理を妨げません**。
- `BasePlugin.on_installation_revoked` を提供する SDK バージョンが必要です。それ以前のバージョンではこのフックは呼ばれません。
### 許可 / 禁止の対照表
| 項目 | 許可 | 禁止 |
| --- | --- | --- |
| 設定の読み取り | 呼び出しごとに読み取る(`self.get_plugin_config()`、`ctx.config`) | `initialize()` で読み取り `self` に保持する |
| コネクションプール / HTTP クライアント | テナント資格情報を含まない共有プール | インストールの endpoint や api key をインスタンスに固定する |
| キャッシュ | artifact レベルの公開データ、または完全な installation 識別子をキーとし解放可能なもの | Workspace やユーザーをキーにしたモジュールレベルの `dict` を長期間保持する |
| 呼び出しをまたぐ状態 | Host サービス(Runner は `ctx.state_*`) | インスタンス属性、モジュールグローバル |
| バックグラウンド処理 | なし | ハンドラーが戻った後もそのコンテキストや実行関連の呼び出しを保持する |
| ログ | `ctx.log()` または SDK のログ API | モジュールレベルのバックグラウンドスレッドからテナント設定や資格情報を出力する |
### よくある反例
**1. 初期化時にインストール設定を読み取る**
```python
async def initialize(self):
self.endpoint = self.plugin.config["endpoint"] # プロセス単位の初期化。現在のインストールは存在しない
self.api_key = self.plugin.config["api_key"]
```
共有初期化の段階にはインストール設定がないため、空の値または前回の値しか読めません。
**2. テナントデータをモジュールグローバルにキャッシュする**
```python
_CACHE = {} # Workspace やユーザーをキーにすると、インストールやテナントをまたいで残留する
async def call(self, params, **kwargs):
if params["user"] not in _CACHE:
_CACHE[params["user"]] = await fetch(params["user"])
```
**3. ハンドラーが戻った後も処理を続ける**
```python
async def call(self, params, **kwargs):
config = self.get_plugin_config()
asyncio.create_task(self._upload_later(config)) # ハンドラーは既に戻っており、コンテキストと権限は無効
return ok()
```
**4. インスタンス属性を設定の入れ物として扱う**
```python
async def initialize(self):
self.default_endpoint = "https://api.example.com" # 公開定数は問題ない
self.last_config = {} # インストール設定を書き込むと違反になる
```
正しい形:
```python
class LookupTool(Tool):
async def initialize(self):
# artifact レベルでテナント情報を含まないオブジェクトのみを置く
self.transport = StatelessHTTPTransport()
async def call(self, params, **kwargs):
config = self.get_plugin_config() # 呼び出しごとに現在のインストール設定を読み取る
return await self.transport.lookup(config["endpoint"], params)
```
### Runner コンポーネントの注意点
Runner はステートレス規約との緊張が最も大きくなります。実行器はセッションやコンテキストをプロセス内に保持しがちだからです。
- `RunnerContext` は**今回の実行中のみ有効**です。コンテキストやその要素をインスタンスに保存して呼び出しをまたいで再利用しないでください。
- Runner が呼び出しをまたいで保持する必要があるセッション状態は、`ctx.state_get()` / `ctx.state_set()`(スコープは `conversation`、`actor`、`subject`、`runner`)または Host が提供するその他のストレージに置き、インスタンス属性は使わないでください。
- ハンドラーが戻った後、バックグラウンドタスクで `ctx` を使い続けたり、`self.plugin` 経由の実行関連呼び出しを使い続けたりしないでください。
- 設計上どうしてもプロセス内の呼び出しをまたぐ状態(長期間常駐する外部セッションプロセスなど)に依存する場合、**`stateless-v1` を宣言しないでください**。専用 Worker のままにします。
コンテキスト API の全体は[実行コンテキスト(RunnerContext)](/ja/plugin/dev/apis/agent-run)を参照してください。
## 自己チェックと提出
提出前に 1 つずつ確認してください。
- [ ] `manifest.yaml` に 2 つのフィールドが正しい値で宣言されている。
- [ ] リポジトリ全体を検索し、モジュールレベルの可変グローバルが存在しないことを確認した。
- [ ] `initialize()` はプロセス単位の初期化のみを行い、インストール設定を読み取っていない。
- [ ] すべてのインストール設定を呼び出し時に読み取っている。
- [ ] ハンドラーが戻った後に動作し続けるタスクやスレッドがない。
- [ ] 呼び出しをまたぐ状態は Host API 経由で保存している(Runner は `ctx.state_*`)。
### 提出方法
認証は公開と同じ経路で、追加の手続きはありません。`manifest.yaml` に上記 2 つのフィールドを宣言し、いつもどおり `lbp publish` で**新しいバージョン**を提出します(公開済みのバージョンに後から宣言を追加することはできません)。LangBot Space は提出された正確なパッケージを確認し、補助的なコードレビューを行い、人による承認後に認証プラグインの表示を発行します。認証は審査済みのバージョンとチェックサムに紐付きます。
要件を満たせないと判断された場合は、まず署名なしの通常のプラグインとして公開し、後続の不変バージョンで修正してください。
このチェックで共有実行に不向きだと分かった場合は、通常のプラグインとして提出してください。認証は必須ではありません。公開の流れは[マーケットプレイスへの公開](/ja/plugin/dev/publish/market)を参照してください。