LangBot Docs

認証プラグインと共有実行

認証とは何か、なぜ存在するのか、いつ必要か、そして 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 つのフィールドを同時に宣言します。

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.sharedRuntimeshared-runtime-v1この値のみ受け付けます。他の値を書くとマニフェスト検証に失敗します
execution.componentModelstateless-v1この値のみ受け付けます
  • 2 つのフィールドは必ず同時に宣言します。sharedRuntime だけを宣言し componentModel を書かないパッケージは、認証検証の段階で拒否されます。
  • これらのフィールドは SDK 0.7.0 以降で認識されます。それ以前の SDK は無視するため、古い SDK で宣言の有効性を確認しないでください。

宣言は申請であり、承認ではありません。宣言だけでは認証表示も付与されず、実行モードも変わりません。

ステートレス規約

  • 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 単位のプロセス内状態を自分で解放する必要があります。

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. 初期化時にインストール設定を読み取る

async def initialize(self):
    self.endpoint = self.plugin.config["endpoint"]  # プロセス単位の初期化。現在のインストールは存在しない
    self.api_key = self.plugin.config["api_key"]

共有初期化の段階にはインストール設定がないため、空の値または前回の値しか読めません。

2. テナントデータをモジュールグローバルにキャッシュする

_CACHE = {}  # Workspace やユーザーをキーにすると、インストールやテナントをまたいで残留する

async def call(self, params, **kwargs):
    if params["user"] not in _CACHE:
        _CACHE[params["user"]] = await fetch(params["user"])

3. ハンドラーが戻った後も処理を続ける

async def call(self, params, **kwargs):
    config = self.get_plugin_config()
    asyncio.create_task(self._upload_later(config))  # ハンドラーは既に戻っており、コンテキストと権限は無効
    return ok()

4. インスタンス属性を設定の入れ物として扱う

async def initialize(self):
    self.default_endpoint = "https://api.example.com"  # 公開定数は問題ない
    self.last_config = {}  # インストール設定を書き込むと違反になる

正しい形:

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)を参照してください。

自己チェックと提出

提出前に 1 つずつ確認してください。

  • manifest.yaml に 2 つのフィールドが正しい値で宣言されている。
  • リポジトリ全体を検索し、モジュールレベルの可変グローバルが存在しないことを確認した。
  • initialize() はプロセス単位の初期化のみを行い、インストール設定を読み取っていない。
  • すべてのインストール設定を呼び出し時に読み取っている。
  • ハンドラーが戻った後に動作し続けるタスクやスレッドがない。
  • 呼び出しをまたぐ状態は Host API 経由で保存している(Runner は ctx.state_*)。

提出方法

認証は公開と同じ経路で、追加の手続きはありません。manifest.yaml に上記 2 つのフィールドを宣言し、いつもどおり lbp publish で新しいバージョンを提出します(公開済みのバージョンに後から宣言を追加することはできません)。LangBot Space は提出された正確なパッケージを確認し、補助的なコードレビューを行い、人による承認後に認証プラグインの表示を発行します。認証は審査済みのバージョンとチェックサムに紐付きます。

要件を満たせないと判断された場合は、まず署名なしの通常のプラグインとして公開し、後続の不変バージョンで修正してください。

このチェックで共有実行に不向きだと分かった場合は、通常のプラグインとして提出してください。認証は必須ではありません。公開の流れはマーケットプレイスへの公開を参照してください。

On this page