メモリプラグインの作成
メモリプラグインはストレージバックエンドです。エージェントが記憶した内容を永続化し、リコールクエリに答えます。これは最もデータモデルが重いプラグインの種類です。ツールが1つの関数を持ち、チャネルがメッセージ形状を持つところ、メモリバックエンドはマルチエージェントの属性付け、名前空間、セッション、カテゴリ、重要度の重み付けを持ち、ランタイムのメモリセマンティクス(スコープ付きリコール、GDPRエクスポート、置換)は、行モデルを正しく実装することに依存しています。
このガイドは、ツールプラグインの基礎と、チャネルガイドで説明したウォームストアのライフサイクルを理解していることを前提としています。内容は wit/v0/memory.wit と、crates/zeroclaw-plugins/src/wasm_memory.rs 内のホストアダプターに照らして検証されています。
配線の状況。
WasmMemoryは、memory-pluginワールドに対してランタイムの完全なMemoryトレイトを実装しており、機能ゲートが掛けられ、ユニットテストと実コンポーネントを使ったエンドツーエンドテストでカバーされています。ランタイムはまだこれを設定可能なバックエンドとして構築していません。ホスト側には、channel_plugin_details()に相当するメモリ側の機能がありません。チャネルの場合と同様に、契約に沿って実装を進めてください。凍結されるのは WIT ワールドとアダプターの振る舞いの部分です。メモリワールドにはまだ設定エクスポートがないため、config_readは要求しないでください。型付きの設定 ABI とリゾルバがWasmMemoryに配線された後にのみ追加します。
データモデル
1つのレコード型が両方向に境界をまたぎます。memory-entry(memory.wit)。ストレージを設計する前にそのフィールドを把握してください。オプションのメソッドはすべてそれらに対するビューだからです:
| フィールド | 意味 |
|---|---|
id | 行の識別子。 |
key | ルックアップキー。一意ではない: 複数の行がキーを共有する場合があり、エージェントごとに1つです。ストレージは key ではなく (key, agent-id) でキー付けする必要があります。 |
content | 記憶されたテキスト。 |
category | core(長期的な事実)、daily(セッションログ)、conversation(コンテキスト)、または custom(string)。 |
timestamp | RFC 3339 の作成時刻。時間範囲の再現境界は両端を含みます。 |
session-id | オプションの会話スコープ。 |
namespace | エージェントまたはコンテキスト間の分離境界。 |
score | 取得関連度 0.0〜1.0。非ベクトル再現の場合は none。 |
importance | 任意の優先順位付けの重み 0.0-1.0. |
superseded-by | このエントリを置き換えたエントリの ID(ある場合)。 |
agent-alias / agent-id | 表示名と生のストレージ識別子。スコープの等価性チェックには agent-id を、表示には agent-alias を使用します。 |
(key, agent-id) の複合キーは、最もよくある間違いです。ベースの get の契約では、次のように明示されています。複数の行が同じキーを共有する場合、任意の一致する行が返され、エージェントスコープのルックアップは get-for-agent を経由します。同様に、forget は帰属に関係なくキーに対応するすべての行を削除しますが、forget-for-agent は (key, agent-id) の行を正確に削除し、兄弟の行はそのまま残します。
必須エクスポート
12個の関数にはデフォルトがなく、動作する必要があります(memory.wit、required-methods セクション):
| エクスポート | 契約ノート |
|---|---|
name | バックエンド名。 |
get-memory-capabilities | オプションのメソッドのビットマスク。ロード時に一度だけ読み取る。 |
store-entry | (key, content, category, session-id) を保存します。store は wit-bindgen で予約されているため、store-entry という名前になっています。 |
recall | クエリ + 上限 + オプションのセッションおよび RFC 3339 の時間範囲(両端を含む)。空のクエリまたは単なる * のクエリは時間のみのリコールを意味し、最新のエントリを返します。 |
get | キー別; マルチエージェントのキー衝突時は任意の行。 |
list-entries | オプションのカテゴリとセッションのフィルター。wit 予約の list にちなんで命名されています。 |
forget | キーのすべての行を削除する。何かが削除された場合は true。 |
forget-for-agent | (key, agent-id) の行のみを削除します。 |
count | エントリの合計。 |
health-check | 到達可能性。 |
store-with-agent / recall-for-agents | 属性認識ペア。以下を参照してください。 |
recall-for-agents は agent-filter バリアントを受け取ります。all(エージェントフィルターなし)または some(list<string>)(リストされたエージェント ID に限定)のいずれかです。ランタイムは Rust の &[&str] スライスを、空スライスは all を意味するようにマッピングするため、some([]) はすべてではなく何にも一致しないものとして扱ってください。
機能フラグ: 11 個のオプションメソッド
チャネルと同じ仕組みです。ホストは get-memory-capabilities を一度だけ読み取り、設定されていない各フラグについては、あなたを呼び出す代わりに Rust トレイトのデフォルトを使用します。デフォルトは memory.wit 内のフラグの隣にインラインで記載されており、ホスト側のフォールバックは wasm_memory.rs で確認できます(各ゲート付きメソッドはフラグをチェックし、フラグがない場合はフォールバックパスを取ります)。
| フラグ | 未設定時のホストフォールバック |
|---|---|
get-for-agent | ホストは get + agent-id 等価フィルタを構成する |
purge-namespace, purge-session, purge-session-for-agent, purge-agent | ホストは “not supported” を返します |
reindex | ホストは 0 を返します |
store-procedural | ホスト no-ops |
ensure-agent-uuid | ホストはエイリアスを変更せずにエコーします |
recall-namespaced | ホストが recall を呼び出し、名前空間でポストフィルタする |
export-entries | ホストは list-entries を呼び出し、ポストフィルタリングします |
store-with-metadata | ホストは store-entry に委譲し、namespace と importance を破棄します |
最後の行をもう一度読んでください。store-with-metadata を実装しない場合、ランタイムが要求した名前空間と重要度はフォールバックによってサイレントに破棄されます。名前空間付きのデータを保存するバックエンドは、store-with-metadata、recall-namespaced、purge-namespace を一式として実装すべきです。そうしないと、名前空間の分離は静かにポストフィルタリングと不可逆な書き込みへと劣化します。
purge ファミリーはデータ削除のためのインターフェイスです。purge-agent は(ID ではなく)agent-alias を受け取ります。export-entries は GDPR 第 20 条のデータポータビリティに対応するために存在し、作成時刻の昇順でエントリを返し、埋め込みは除外する必要があります。バックエンドが実際のユーザーデータを扱う場合は、purge および export のフラグを実装してください。「サポートされていない」という回答が許容されるのは、使い捨てのバックエンドの場合のみです。
スケッチ: ストレージの形
コンポーネントのパターンはチャネルと同じ(ウォームインスタンス、thread_local 状態)なので、異なるのはデータレイヤーだけです。最小限で誠実なバックエンドは、正しくキー付けされたインメモリマップです。
#![allow(unused)]
fn main() {
use std::collections::HashMap;
struct Row {
id: String,
content: String,
category: Category,
timestamp: String,
session_id: Option<String>,
namespace: String,
importance: Option<f64>,
superseded_by: Option<String>,
agent_alias: Option<String>,
}
/// (key, agent_id) -> Row。agent_id の None は未帰属の行を表します。
type Table = HashMap<(String, Option<String>), Row>;
}
すべての必須メソッドは、その後、単純な走査になります:
store-entryは名前空間"default"で(key, None)に挿入します。store-with-agentは、呼び出し元の名前空間と重要度を使用して(key, agent-id)に挿入します。recallはcontentに対して部分文字列/ランクでフィルタリングし、セッションフィルタを適用し、timestampに対して RFC 3339 の境界値(両端を含む)を適用し、ソートしてlimitで切り詰めます。空のクエリまたは*クエリは、最新のものから順に処理してください。recall-for-agentsは、キーの2番目のコンポーネントに対するエージェントフィルターのウォークを追加します。
実際のバックエンドでは、コントラクトの形状を変更することなく、マップを組み込みストアに置き換えられます。メモリアダプタは、そのスコープが http_client を持つ場合でも、意図的にまだ wasi:http をリンクしていません。リモートバックエンドには、コンポーネントでテストされた別個のメモリとネットワークの境界が必要です。
ホストが周囲で行うこと
wasm_memory.rs におけるアダプターの動作を知ることで、いくつかのコントラクトのエッジが説明できます:
- ウォームストア、呼び出しごとに補給。 チャンネルと同じ: プラグインのライフタイムに1つのインスタンス、各呼び出しごとに新しい燃料、呼び出しは mutex の背後でシリアライズされます。バックエンドが同時呼び出しを見ることはありません。
- ロード時にキャッシュされるケイパビリティ。 ホストは
from_wasm中にフラグを一度だけ読み取り、その後は二度と読み取りません。動的なケイパビリティ検出はありません。デーモンを再起動することで再読み取りされます。 - 1つのトラップがバックエンドを停止させます。 すべての呼び出し箇所は、名前付きのコンテキスト(
memory.recall-namespaced trappedなど)でトラップをラップします。トラップや燃料(fuel)の枯渇が発生すると、Wasmtime はそのストアへの再進入を拒否します。また、期限切れによって中断されると、コードは途中で割り込まれます。いずれの場合も、ホストはインスタンスを破棄し、そのインスタンスに到達する以降のすべての呼び出しはplugin instance is unavailableで失敗します。Wasmtime の既定値である 1 回の呼び出しで 128 MiB を超えてゲストメモリからコピーする結果など、ホストが読み出せない結果も同様です。ホストはインスタンスを再構築しません。エントリはそのインスタンス内にのみ存在するため、新しいインスタンスは何も保存されていなかったかのように応答します。予期される失敗には panic せず、err(string)を返してください。返したエラーであればインスタンスは維持されます。 - ポストフィルタのフォールバックはホスト側です。
recall-namespacedフラグが未設定の場合、名前空間フィルターはrecallが返した後にホスト上で実行されます。limitの処理がこれと相互作用します。ホストは呼び出し元の limit をrecallに渡すため、ポストフィルタリングによって結果が埋まりきらないことがあります。名前空間付きバリアントをネイティブに実装するもう一つの理由です。
マニフェスト、ビルド、インストール
マニフェストとは、プラグインディレクトリ内にある manifest.toml という名前のファイルです。そのフィールドは crates/zeroclaw-plugins/src/lib.rs 内の PluginManifest の serde サーフェスであり、これが信頼できる情報源となります。
| フィールド | 必須 | 意味 |
|---|---|---|
name | yes | 一意の正規パッケージスラッグであり、派生する各インスタンス設定キーのパッケージ部分です。これはそれ自体がオペレーター設定キーではありません。1~128文字の小文字 ASCII 文字を使用し、先頭と末尾は [a-z0-9] とし、その間には [a-z0-9._-] のみを使用します。検出時に無効な名前や重複した名前は拒否されます。 |
version | yes | バージョン文字列(例:0.1.0)。 |
description | いいえ | zeroclaw plugin list によって表示される人間が読める説明。 |
author | いいえ | 作者名または組織。 |
wasm_path | WASM 機能について | コンポーネントのファイル名(プラグインディレクトリからの相対パス)。唯一の capability が skill の場合を除き必須。指定されたファイルが存在しない場合、検出はプラグインをスキップします。 |
wasm_sha256 | |in strict mode for WASM capabilities WASM機能の厳格モードで| |-| | |Hexadecimal SHA-256 of the exact component bytes. When present it is always enforced; strict signature policy requires it so the signed manifest binds the admitted executable. |
Wait — that’s the source text itself, and I shouldn’t return it verbatim. Here is the Japanese translation:
コンポーネントのバイト列そのものに対する16進数表記のSHA-256ハッシュ。指定されている場合は常に強制されます。厳格な署名ポリシーでは、署名済みマニフェストが許可された実行ファイルに紐付けられるよう、これが必須です。|
|capabilities|はい、空ではありません|プラグインの種類: tool、channel、memory、observer、skill のいずれか(PluginCapability、snake_case でシリアライズ)。|
|provides|いいえ|channel プラグインで、コンパイル済みのチャネルのドロップイン代替となるもの。ミラーする snake_case の設定 ID は telegram、gmail_push です。ホストは、有効なエージェントがルーティングする、設定済みかつ有効な [channels.<id>.<alias>] ごとに論理インスタンスを1つだけ受け入れるため、正規のチャネル設定が、これらの設定の唯一の保管場所であり続けます。config_read が必要です。plugin ファミリーは予約されており拒否されます。本ビルドで定義されていない ID は拒否されます。同じ ID を主張する2つのインストール済みパッケージは、両方とも拒否されます。省略時(デフォルト)は、[channels.plugin.<alias>] から設定される新規チャネルを意味します。ミラーは受け入れられますが、まだ構築されていません。アクティベーション計画については crates/zeroclaw-runtime/src/plugin_runtime.rs を参照してください。|
|permissions|いいえ|コードが到達しうるホストサービス: http_client、config_read、state_read、state_write、file_read、file_write、memory_read、memory_write(PluginPermission)。HTTP、config、および正確なインスタンス単位の state の権限は、ツール/チャネルアダプターに対して適用されます。ファイルシステムおよびメモリの権限は無効のままです。config_read を宣言するには config_schema が必要です。|
|config_schema|config_read と完全に一致して|このプラグインのプライベート設定用の Draft 2020-12 JSON Schema。canonical manifest バイト列に含まれるため、マニフェストの署名時に対象となります。ルートは properties マップと additionalProperties = false を持つオブジェクトでなければなりません。すべてのトップレベルプロパティは、直接または、ローカルの JSON Pointer を介して、明示的にサポートされる型を1つ持たなければなりません: string、boolean、integer、number、array、または object。ツールおよびチャネルの利用側は、トップレベルの文字列プロパティに直接 x-secret = true を設定することができます。これにより、そのプロパティは公開設定から削除され、スコープ付きの secrets.get ホストインポートを通じて公開されます。secret プロパティ名は、1~128 バイトの移植性のある ASCII 名であり、英字、数字、_、-、. のみを含みます。ツールは公開設定を __config の下で受け取り、execute の実行中にシークレットを読み取ることができます。チャネルは configure および運用時の呼び出し中に、config.get を通じて現在の公開オブジェクトを読み取り、secrets.get を通じてシークレットを読み取ります。インスタンス化および静的メタデータ検出の間は、両方のインポートは利用できません。ネストされた、false の、または非ブール値の secret マーカー、および文字列以外の secret プロパティは拒否されます。config_read のないスキーマ、またはスキーマのない config_read は拒否されます。|
|signature|いいえ|正規化されたマニフェストバイト列に対する Base64url Ed25519 署名。配布用に署名する際に設定されます。|
|publisher_key|いいえ|署名者の16進数エンコードされたEd25519公開鍵。|
|エグレス|いいえ|任意の [egress] テーブル。hosts リストを保持し、このプラグインが必要とする接続先を宣言します。指定できるのは完全一致のホスト(api.example.com)か、明示的なサフィックスパターン(*.example.com)で、後者はサブドメインには一致しますが apex(ドメイン自身)には一致しません。このテーブルは正規化されたマニフェストのバイト列の一部であるため、署名時に保護対象となり、変更するには再署名が必要です。不正な文法は、検出時およびインストール時にマニフェスト全体を拒否します。省略された状態、空の状態、「何も宣言しない」状態は同一として扱われます。|
コードが実際に使用する権限のみを宣言してください。宣言されていない権限はコンポーネントが到達できないホストサーフェスであり、不要に宣言された権限は自ら求めた攻撃対象領域であり、プラグインをレビューする人にとっての監査負担です。
[egress] は宣言であり、権限の付与ではありません。マニフェストによってネットワークへの到達性が与えられることはありません。ホストが適用する許可リストは、インスタンス自身の zpi1_… 行に対するオペレーターの plugins.entries[].egress_hosts であるため、独自の [egress] テーブルを書いた未署名のコンポーネントであっても、どこにも到達できません。宣言が果たす役割はインストール時の手続きです。zeroclaw plugin install はそれを、同コマンドが作成する行に設定し、何が付与されたかを表示します。既存の行を広げることは決してありません。詳しくは Declaring and granting egress を参照してください。
オペレーター値は plugins.entries 内では文字列のまま保持され、永続化時には、ホスト所有のパッケージ、ケイパビリティ、バインディングの識別情報から導出されたバージョン付きの zpi1_… 文字列をキーとして暗号化されます(インストール時に出力され、デフォルトのツールバインディングのフルインスタンスキーにシードされます)。文字列はそのまま保存され、ブール値と数値には JSON スカラーのテキストが使用され、配列とオブジェクトには JSON テキストが使用されます。ゲストコードが実行される前に、ホストはそれらの文字列をパッケージスキーマの型に実体化し、ツールアダプターとチャンネルアダプターについてオブジェクト全体を検証します。シークレットではないツールプロパティで __config が構成され、チャンネルは config.get を介してシークレットではないオブジェクトを取得します。x-secret = true とマークされたプロパティは両方の公開サーフェスから省略され、認可されたサービスフレーム内で secrets.get("property") を介してのみ利用できます。1 回の呼び出し中のチャンネルの公開読み取りとシークレット読み取りは単一の正規リビジョンを共有し、ホストは呼び出しの終了時にその実体化されたビューを破棄します。準拠するチャンネルプラグインは、利用する各時点でその両方を 必ず 解決し、ウォームなゲスト状態に設定や資格情報の値を保持してはなりません。ゲストに平文を返すと、ホストは悪意のあるコードに対して値を保持しないことを強制できなくなります。config_read が要求されたものの実効的に許可されなかった場合、ホストは空のオブジェクトを検証します。そのため、必須プロパティを持つスキーマは、必須設定なしで起動するのではなくフェイルクローズします。空のオブジェクトが有効な場合、ツールは空の __config を省略し、チャンネルの設定/シークレットインポートは access-denied を返します。認可されたフレーム外の呼び出し、解決失敗、ホスト呼び出し予算の枯渇は unavailable を返します。
メモリバックエンドの場合: capabilities に memory を含めます。まだ config_read を要求しないでください: 受け入れにはスキーマが必要ですが、現在のメモリワールドには、ホストが結果のオブジェクトを渡すためのエクスポートがありません。http_client にも依存しないでください: 権限付与だけではメモリアダプターの範囲を広げられず、現在はネットワークサーフェスを公開していません。
WASI Preview 2 ターゲットを一度インストールしてから、コンポーネントをビルドします:
rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2
コンポーネントは target/wasm32-wasip2/release/<crate_name>.wasm に生成されます(クレート名のハイフンはアンダースコアに変換されます)。プラグインディレクトリを構成する際は、マニフェストの wasm_path で宣言した名前に変更してください。
[!IMPORTANT] コンパイル済みの
.wasmおよび.cwasmファイルはバイナリ成果物であり、多くの場合それぞれ数メガバイトになります。Git LFS を使わずに Git のソースツリーへコミットしないでください。リビルドのたびに素のブロブとしてコミットされると、リポジトリの履歴が永久的に肥大化し、git diffやレビューツールがそれらの処理でつまずきます。これらは他のビルド出力と同様に扱ってください。target/と*.wasm/*.cwasmを.gitignoreに追加し、代わりにリリース成果物やプラグインレジストリのアーカイブを通じて配布してください。どうしても成果物をツリー内に置く必要がある場合は、最初のコミットの前に LFS でパターンを追跡してください(git lfs track "*.wasm")。
ターゲットホストがランタイム専用ビルド(JITバックエンドがコンパイルに含まれていない)の場合、ロード時に .wasm をコンパイルできません。代わりに事前コンパイル済みの .cwasm をデシリアライズします。ホストのバージョンと一致する wasmtime CLI で事前コンパイルし、.cwasm を wasm_path アーティファクトとして同梱してください。バージョンが一致しないアーティファクトは、wasmtime のデシリアライズチェックによって拒否され、サイレントに誤ロードされることはありません。
これらのコマンドには、プラグインホストがコンパイルに含まれたバイナリが必要です。 64ビットのデスクトップ・サーバー向けターゲット(glibc または musl の x86_64 および aarch64 Linux、Apple silicon と Intel の macOS、x86_64 の Windows)のリリースアーカイブにはこれが含まれており、各リリースは公開前に、それぞれのアーカイブからプラグインを1つ実行します。32ビット ARM のアーカイブ2種と実験的な Android アーカイブには含まれておらず、Cargo のデフォルト設定によるソースビルド(
cargo install zeroclaw、install.sh --source、または features なしのcargo build)にも含まれていません。これらの場合、zeroclaw plugin ...は認識されないサブコマンドとなり、インストール済みのプラグインは検出されません。そのような環境では、プラグイン実行バックエンドを有効にしてソースからビルドしてください(例えばcargo build --release --features plugins-wasm-cranelift)。アーティファクトごとのプラグイン対応状況は、プロトコルのページに記載されています。
各プラグインは、プラグインディレクトリ(デフォルト ~/.zeroclaw/plugins/、plugins.plugins_dir を通じて解決)の独自のサブディレクトリに配置され、マニフェストとマニフェストの wasm_path に一致する名前のコンポーネントを保持します:
~/.zeroclaw/plugins/
└── my-plugin/
├── manifest.toml
└── my-plugin.wasm
ローカルディレクトリからインストール(これはマニフェストの形状を検証し、何かをコピーする前に署名ポリシーを実行します):
zeroclaw plugin install ./my-plugin/
プラグインシステムを有効にして検出を確認する:
zeroclaw config set plugins.enabled true
zeroclaw plugin list
zeroclaw plugin info my-plugin
zeroclaw plugin list と zeroclaw plugin info は、パッケージがインストールされ、検出可能であることを確認しますが、検出は有効化を意味しません。plugins.enabled = true によってプラグインホストが有効になります。自動検出されたツールおよびスキルの機能は、plugins.auto_discover = true も設定されている場合にのみ実行時に読み込まれ、このフラグはデフォルトでは false です(フェイルクローズド):
zeroclaw config set plugins.auto_discover true
したがって、plugins.enabled = true だけでは、[channels.plugin.<alias>] の下で宣言したチャンネルのみが有効になり、プラグインのツールやスキルは有効になりません。ツールまたはスキルのパッケージが zeroclaw plugin list に表示されていても、実行時には何も提供しないことがあります。明示的なチャンネルバインディングは自動検出ではなくオペレーターが名前を指定するものなので、auto_discover は不要です。このフラグが制御するのは、自動検出されたツールとスキルだけです。
zeroclaw plugin list にないプラグインは検出時にスキップされました: 起動ログでスキップ警告を確認してください(形式が不正なマニフェスト、wasm_path ファイルの欠落、または署名ポリシーによる拒否)。