Python particleを作成する
パーティクル(Particle)は、ツール(JSONスキーマで定義された入力を持つ名前付き操作)をパーティクルランタイムに公開する単一ファイルプログラムです。このスキルでは、ビルドして実行可能なParticleを作成するために必要なすべてを説明します。
1. ファイル構造
プロジェクトルートに単一のソースファイルを置き、名前は正確に Particlefile.py とします。他に必要なものはありません。PyPI依存関係は、ファイルの先頭にあるPEP 723形式のインラインメタデータブロックで宣言します。ビルド時に解決されます。pyproject.toml も仮想環境も管理する必要はありません。
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "idna>=3", # オプション — サードパーティパッケージを使用する場合のみ
# ]
# ///
2. モジュールレベルの particle — 全API
ファイルは、Particle インスタンスをモジュールレベルの particle 変数に代入する必要があります。ランタイムはビルド時にこれをイントロスペクトし、すべてのツール呼び出しでハンドラを読み取ります。
from particle.manifest import (
Particle, Tool, Http, Filesystem, Mount, TempMount,
Credential, OAuth2, ApiKey, ApiKeyLocation, SigningKey, Basic, Raw,
)
from particle import http, credentials, kv, oauth, signing
def _do_thing(args):
return {"result": f"hello {args['name']}"}
def _ping():
return {"status": "ok", "message": "alive"}
particle = Particle(
name="kebab-case-name", # 必須、kebab-case
description="...", # 必須、一行
version="0.1.0", # 必須、セマンティックバージョニング
# アウトバウンドHTTP許可リスト。ここにリストされていないホストへのリクエストは
# ネットワーク境界で拒否されます。パーティクルが一切の
# アウトバウンドリクエストを行わない場合は `http=` を完全に省略します。
http=Http(allowed_hosts=["api.example.com"]),
# ホストディレクトリアクセス、デフォルトではオフ。下記「ファイルシステム」を参照。
# filesystem=Filesystem(mounts={...}, temp={...}),
# オプション。名前ごとに宣言。ユーザーはセットアップ時に方法を選択します。
credentials={
"example": Credential(
hosts=["api.example.com"],
required=True,
methods={
# 下記「認証方法」を参照
},
),
},
# 必須。名前 → Tool(description, input_schema, handler)。
tools={
"do_thing": Tool(
description="このツールが何をするか — 一行で、LLM向けに記述します。",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "..."},
},
"required": ["name"],
},
handler=_do_thing,
),
},
# オプション。`particle ping` で呼び出されます。
ping=_ping,
)
ツール
description: LLM呼び出し元向けに記述 — 短く、アクション指向に。input_schema: JSON Schema Draft 2020-12、オブジェクトルート。ホストはハンドラを呼び出す前に引数を検証するため、dictの形状が正しいことを信頼できます。handler: 1つの位置引数(dict)を受け取り、JSONシリアライズ可能な任意の値を返すプレーンな呼び出し可能オブジェクト。defまたはasync defのいずれか。asyncハンドラはワンショットのポールループで実行されます。エラーを表面化するには例外を発生させます。
Ping
オプション。以下のようなdictを返します:
{"status": "ok" | "degraded" | "unhealthy",
"message": "...", # オプション
"details": ...} # オプション、JSONシリアライズ可能
3. ハンドラから利用可能なランタイムAPI
すべてのホスト機能は from particle import ... にあります。これらが唯一必要な非stdlibモジュールです。
アウトバウンドHTTP
HTTP呼び出しを行うための、同等の4つの方法。コードの残りの部分に合ったイディオムを選んでください。すべてホストの許可リストを経由します。
particle.http.fetch — ネイティブAPI。credential_name= が設定されている場合、認証情報を自動的に適用します。
from particle import http
res = http.fetch(
"https://api.example.com/things",
method="POST",
headers={"Content-Type": "application/json"},
body=json.dumps({...}).encode(),
credential_name="example", # オプション。ホストがシークレットを置き換えます
)
if not res.ok:
raise RuntimeError(f"upstream {res.status_code}")
data = res.json()
http.fetch は以下を持つ Response を返します:
.status_code(int),.ok(bool),.headers((name, bytes)タプルのリスト),.header(name)ルックアップ,.body(bytes),.text(str),.json()(パース済み)。
urllib.request.urlopen — そのまま動作します。ランタイムが wasi:http を通るようにモンキーパッチします。
import urllib.request
with urllib.request.urlopen("https://api.example.com/x") as r:
body = r.read()
urllib3 / requests — そのまま動作します。ランタイムがurllib3のインポート時にカスタムコネクションクラスを注入するため、requests.get(...) などがそのまま動作します。認証情報は自動的に適用されません — 認証されたリクエストには、http.fetch(..., credential_name=...) を使用するか、プレースホルダを取得して自分でヘッダーを設定してください。
import requests
r = requests.get("https://api.example.com/x") # 動作します
httpx — そのまま動作します、同期と非同期の両方。インポート時にカスタム HTTPTransport / AsyncHTTPTransport が注入されます。
import httpx
r = httpx.get("https://api.example.com/x")
生のソケットは利用できません。 socket.socket() は OSError を発生させます。
ファイルシステム
デフォルトではオフ — ハンドラは自身のバンドルのみを参照できます。Filesystem 機能を宣言してホストディレクトリアクセスを取得し、通常の open() / pathlib で読み書きします(ランタイムが wasi:filesystem を通じてルーティングします)。
from particle.manifest import Filesystem, Mount, TempMount
filesystem=Filesystem(
# *ユーザー*が実際のパスにマッピングするホストディレクトリ — 永続的には
# `particle mount <particle> <name> <host-path>` で、実行時には
# `--mount <name>=<host-path>` でマッピングします。
mounts={
"data": Mount(
description="レポートが書き込まれる場所", # インストールプロンプトに表示
path="/mnt/data", # サンドボックス内の絶対パス
access="readwrite", # "readonly" | "readwrite"
required=True, # マッピングされるまで実行を拒否(デフォルトFalse)
),
},
# ホストが実行ごとに新しくプロビジョニングし、終了時にクリアするスクラッチ領域。
# 常に読み書き可能。ユーザーがマッピングすることはありません。
temp={
"work": TempMount(description="スクラッチ領域", path="/tmp/work", max_size="10MB"),
},
),
with open("/mnt/data/in.json") as f:
data = f.read()
with open("/mnt/data/out.json", "w") as f:
f.write(result)
- ハンドラは宣言された
pathを直接開きます。マウントの名前はサンドボックス内に現れません。 readonlyマウントへの書き込み、または宣言されたマウント外のパスへの読み取り/書き込みはOSErrorを発生させます。- ユーザーが決してマッピングしない
requiredマウントは、ビルド/インストール時ではなく実行時に失敗します。 max_sizeはオプションでKB/MB/GBサフィックス("10MB")が付いたバイト数です。これを超える書き込みは失敗します。
認証情報
from particle import credentials
# ユーザーがセットアップ時に選択した方法("oauth"、"pat"など)またはNone。
method = credentials.get_configured_method("example")
# タイプが"raw"の認証情報の生の値。
value = credentials.get_raw("some-raw-cred")
# 低レベル:不透明なプレースホルダと、ホストが置換を期待する場所を示す適用仕様を取得。
# ほとんどの場合、代わりに内部でこれを行う `http.fetch(..., credential_name=...)` を使用します。
info = credentials.get_placeholder("example")
キー/バリューストア
パーティクルごとにスコープ。文字列のみ。バイナリの場合は必要に応じてbase64エンコード。Particle で kv=KV(enabled=True) と宣言します — 宣言がないと、すべての呼び出しが not-declared エラーを発生させます(ユーザーの承認は不要、宣言のみが必要です):
from particle.manifest import Particle, KV
# particle = Particle(..., kv=KV(enabled=True))
from particle import kv
kv.set("cursor", "abc123")
v = kv.get("cursor") # str | None
kv.delete("cursor")
keys = kv.list("prefix:") # list[str] — 注意:組み込みを隠蔽
モジュールを kv としてインポートします(from particle.kv import list ではなく)。これにより list が組み込みを隠蔽しません。
OAuthリフレッシュ
OAuth2 認証情報がある場合のみ。credential_name= を指定した http.fetch の呼び出しは期限切れのトークンを自動的にリフレッシュします。これは、まだ有効だと思っていたトークンを上流が拒否した場合にのみ呼び出します。
from particle import oauth
oauth.refresh("example")
署名
SigningKey 認証情報の場合のみ。キーマテリアルがPythonに渡されることはありません。
from particle import signing
sig = signing.sign("webhook-key", payload_bytes)
ok = signing.verify("webhook-key", data_bytes, signature_bytes)
4. 認証方法
各 Credential には methods={...} マップがあります。ユーザーはセットアップ時に1つを選択します。データクラスタイプは particle.manifest からインポートします。
OAuth2
"oauth": OAuth2(
description="OAuthでサインイン",
flows=["authorization-code", "device-code"],
scopes=["read", "write"],
authorization_url="https://provider/oauth/authorize",
token_url="https://provider/oauth/token",
device_auth_url="https://provider/oauth/device/code", # "device-code" の場合
),
有効なフロー文字列: "authorization-code"、"authorization-code-pkce"、"device-code"
ApiKey
"pat": ApiKey(
description="個人アクセストークンを使用",
location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),
ApiKeyLocation.kind は以下のいずれか:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(例:Bearer)"query-param"+name:?<name>=<key>として追加
location を完全に省略すると、セットアップ時にインポーターがユーザーにプロンプトを表示します。
Basic
"basic_auth": Basic(description="ユーザー名 + パスワード"),
Authorization: Basic <base64> に置き換えられます。
SigningKey
"webhook": SigningKey(description="...", algorithm="hmac-sha256"),
有効: "hmac-sha256"、"hmac-sha512"。signing.sign を介して使用します。
Raw
"api_key_blob": Raw(description="..."),
credentials.get_raw(name) を介して使用します。生のバイトが本当に必要な場合にのみこれを選択します — 他のタイプは値がPythonメモリに決して入らないため、より安全です。
5. PyPI依存関係
ファイルの先頭にあるPEP 723ブロックで宣言します:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx>=0.27",
# "pyjwt>=2",
# ]
# ///
制約:
- Pure-Pythonホイールのみ。 C拡張を必要とするもの(numpy、pydantic v1、lxmlなど)はロードされません。
- 標準ライブラリはほとんど利用可能 —
json、re、hashlib、hmac、base64、secrets、datetime、urllib.parse、asyncio、emailなどはすべて動作します。 ssl、生のsocket、subprocess— 利用できません。open()/pathlibは宣言されたファイルシステムマウント(上記「ファイルシステム」を参照)およびパーティクル自身の/particle/バンドル内のパスに対してのみ動作します。任意のホストパスは拒否されます。
6. 重要なルール
-
モジュールスコープでのホスト呼び出し禁止。 ファイルの先頭で
http.fetch、kv.get、credentials.get_placeholderなどを呼び出すと、ビルド時に失敗します(イントロスペクトフェーズでは、モジュールがトラップストアの下で実行されます)。すべてのホスト呼び出しはhandlerまたはping関数内で行ってください。# ✗ ビルドに失敗します _cached_data = http.fetch("https://api.example.com/init") particle = Particle(...) # ✓ ハンドラ内で def _go(args): res = http.fetch("https://api.example.com/...") ... particle = Particle(tools={"go": Tool(..., handler=_go)}) -
取得するすべてのホストを宣言する。
http=Http(allowed_hosts=[...])にないホストへのリクエストは拒否されます。エラーはハンドラ内で例外として表面化します。 -
Credential.hostsはHTTP許可リストのサブセットである必要があります。 ビルドは、パーティクルが到達できないホストにバインドされた認証情報を拒否します。 -
nameはkebab-case(小文字、ハイフン)である必要があります。 レジストリキーは(name, version)です。 -
versionは有効なセマンティックバージョニングである必要があります。0.1.0、1.2.3-rc.1など。 -
ツールハンドラはJSONシリアライズ可能な値を返す必要があります。 プレーンな
dict、list、str、int、float、bool、None。データクラスやdatetimeはシリアライズに失敗します — 戻す前に変換してください。
7. ワークフロー
Particlefile.py の記述後:
particle build # パース、依存関係解決、イントロスペクト、登録
particle ping <name> # pingを確認(定義されている場合)
particle run <name> # ツール一覧表示
particle run <name> <tool> --help # ツールのフラグを表示
particle run <name> <tool> --foo=bar # 実行
particle build は、最初の実行時にインタラクティブに認証情報のセットアップをユーザーに案内します。ビルドが失敗した場合、エラーはフェーズ(import-scan、resolve-and-fetch、manifest-extract)と具体的な問題を示します。
パーティクルがファイルシステムマウントを宣言している場合、実行前にマッピングするか(または1回限りのために particle run に --mount name=path を渡します):
particle mount <name> <mount-name> <host-path> # 永続的なマッピングを保存
particle mount <name> # マウントと現在のマッピングを一覧表示
パーティクルをスタンドアロン実行可能ファイルとして公開するには(Claude Codeスキルやシェルでの使用に便利)、particle link <name> ./<name> で、引数を particle run <name> に転送するランチャーを作成します。
8. 動作例
GitHubリポジトリ検索ツールを公開する最小限のパーティクル:
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
import json
from particle import http
from particle.manifest import Particle, Tool, Http, Credential, ApiKey, ApiKeyLocation
def _get_repo(args):
res = http.fetch(
f"https://api.github.com/repos/{args['owner']}/{args['repo']}",
headers={"Accept": "application/vnd.github+json"},
credential_name="github",
)
if not res.ok:
raise RuntimeError(f"GitHub {res.status_code}: {res.text}")
r = res.json()
return {
"full_name": r.get("full_name"),
"stars": r.get("stargazers_cou…
…[翻訳のため一部省略]