Ein Python-Particle schreiben
Ein Particle ist ein einzeiliges Programm, das der Partite-Runtime ein oder mehrere Tools (benannte Operationen mit einem JSON-Schema-Input) zur Verfügung stellt. Dieses Skill enthält alles, was du brauchst, um ein solches zu erstellen, das baut und läuft.
1. Datei-Layout
Eine Quelldatei im Projektstamm, exakt mit dem Namen Particlefile.py. Nichts weiter ist erforderlich. PyPI-Abhängigkeiten werden in einem PEP‑723-Inline-Metadatenblock am Dateianfang deklariert; der Build löst sie auf. Kein pyproject.toml, keine virtualenv-Verwaltung.
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "idna>=3", # optional – nur, wenn du Drittanbieterpakete verwendest
# ]
# ///
2. Das modulweite particle – die gesamte API
Die Datei muss eine Particle-Instanz einer modulweiten Variable particle zuweisen. Die Runtime inspiziert diese beim Build und liest bei jedem Tool-Aufruf die Handler aus.
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"hallo {args['name']}"}
def _ping():
return {"status": "ok", "message": "aktiv"}
particle = Particle(
name="kebab-case-name", # erforderlich, kebab-case
description="...", # erforderlich, einzeilig
version="0.1.0", # erforderlich, semver
# Ausgehende HTTP-Erlaubnisliste. Nicht gelistete Hosts werden an der
# Netzwerkgrenze abgelehnt. Lasse `http=` ganz weg, wenn das Particle
# keine ausgehenden Anfragen stellt.
http=Http(allowed_hosts=["api.example.com"]),
# Host-Verzeichniszugriff, standardmäßig deaktiviert. Siehe „Dateisystem“ unten.
# filesystem=Filesystem(mounts={...}, temp={...}),
# Optional. Pro Name deklariert; der Benutzer wählt bei der Einrichtung eine Methode.
credentials={
"example": Credential(
hosts=["api.example.com"],
required=True,
methods={
# siehe „Anmeldedaten-Methoden“ unten
},
),
},
# Erforderlich. Name → Tool(Beschreibung, input_schema, Handler).
tools={
"do_thing": Tool(
description="Was dieses Tool tut – eine Zeile, für ein LLM geschrieben.",
input_schema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "..."},
},
"required": ["name"],
},
handler=_do_thing,
),
},
# Optional. Wird durch `particle ping` aufgerufen.
ping=_ping,
)
Tools
description: Für einen LLM-Aufrufer geschrieben – kurz, handlungsorientiert.input_schema: JSON Schema Draft 2020-12, objektbasiert. Der Host validiert die Argumente dagegen, bevor er deinen Handler aufruft, sodass du innerhalb der Dict-Form vertrauen kannst.handler: Ein einfaches Callable, das ein positionsbasiertes Dict entgegennimmt und einen beliebigen JSON-serialisierbaren Wert zurückgibt. Kanndefoderasync defsein; asynchrone Handler laufen in einer einmaligen Poll-Schleife. Wirf einen Fehler, um einen Fehler anzuzeigen.
Ping
Optional. Gib ein Dict wie dieses zurück:
{"status": "ok" | "degraded" | "unhealthy",
"message": "...", # optional
"details": ...} # optional, JSON-serialisierbar
3. Laufzeit-APIs, die für Handler verfügbar sind
Alle Host-Funktionen sind unter from particle import ... verfügbar. Dies sind die einzigen Nicht-Stdlib-Module, die du benötigst.
Ausgehendes HTTP
Vier gleichwertige Möglichkeiten, einen HTTP-Aufruf zu tätigen. Wähle die, die am besten zum Idiom des restlichen Codes passt; alle werden durch die Host-Erlaubnisliste geleitet.
particle.http.fetch – die native API. Wendet automatisch eine Anmeldedaten an, wenn credential_name= gesetzt ist.
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", # optional; der Host ersetzt das Geheimnis
)
if not res.ok:
raise RuntimeError(f"upstream {res.status_code}")
data = res.json()
http.fetch gibt ein Response-Objekt zurück mit:
.status_code(int),.ok(bool),.headers(Liste von(name, bytes)-Tupeln),.header(name)-Nachschlagefunktion,.body(bytes),.text(str),.json()(geparst).
urllib.request.urlopen – funktioniert wie gewohnt. Die Runtime patcht es, um es durch wasi:http zu leiten.
import urllib.request
with urllib.request.urlopen("https://api.example.com/x") as r:
body = r.read()
urllib3 / requests – funktionieren wie gewohnt. Die Runtime injiziert beim Import von urllib3 eine benutzerdefinierte Verbindungsklasse, sodass requests.get(...) usw. einfach funktionieren. Sie wenden keine Anmeldedaten automatisch an – für authentifizierte Anfragen verwende entweder http.fetch(..., credential_name=...) oder hole den Platzhalter und setze den Header selbst.
import requests
r = requests.get("https://api.example.com/x") # funktioniert
httpx – funktioniert wie gewohnt, sowohl synchron als auch asynchron. Benutzerdefiniertes HTTPTransport / AsyncHTTPTransport wird beim Import injiziert.
import httpx
r = httpx.get("https://api.example.com/x")
Rohe Sockets sind nicht verfügbar. socket.socket() löst OSError aus.
Dateisystem
Standardmäßig deaktiviert – ein Handler sieht nur sein eigenes Bundle. Deklariere eine Filesystem-Fähigkeit, um Host-Verzeichniszugriff zu erhalten, und lies und schreibe dann mit gewöhnlichem open() / pathlib (die Runtime leitet sie durch wasi:filesystem).
from particle.manifest import Filesystem, Mount, TempMount
filesystem=Filesystem(
# Host-Verzeichnisse, die der *Benutzer* auf reale Pfade abbildet – dauerhaft
# mit `particle mount <partikel> <name> <host-pfad>`, oder pro Lauf
# mit `--mount <name>=<host-pfad>`.
mounts={
"data": Mount(
description="Wo Berichte geschrieben werden", # wird im Installationsprompt angezeigt
path="/mnt/data", # absoluter Pfad innerhalb der Sandbox
access="readwrite", # "readonly" | "readwrite"
required=True, # verweigert den Start bis zur Zuordnung (Standard False)
),
},
# Temporärer Speicher, den der Host bei jedem Lauf neu bereitstellt und beim Beenden löscht.
# Immer lese-/schreibbar; der Benutzer ordnet diese nie zu.
temp={
"work": TempMount(description="Arbeitsspeicher", 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)
- Der Handler öffnet den deklarierten
pathdirekt; der Mount-Name erscheint nie innerhalb der Sandbox. - Schreiben auf einen
readonly-Mount oder Lesen/Schreiben eines Pfades außerhalb eines deklarierten Mounts löstOSErroraus. - Ein
required-Mount, den der Benutzer nie zuordnet, schlägt zur Laufzeit fehl, nicht beim Build/Install. max_sizeist eine Byteanzahl mit einem optionalen SuffixKB/MB/GB("10MB"); Schreibvorgänge darüber hinaus schlagen fehl.
Anmeldedaten
from particle import credentials
# Die vom Benutzer bei der Einrichtung gewählte Methode ("oauth", "pat", ...) oder None.
method = credentials.get_configured_method("example")
# Rohwert einer Anmeldedaten vom Typ "raw".
value = credentials.get_raw("some-raw-cred")
# Niedrigstufig: Einen undurchsichtigen Platzhalter + Apply-Spezifikation abrufen, die beschreibt,
# wo der Host ihn ersetzen soll. Fast immer möchtest du stattdessen
# `http.fetch(..., credential_name=...)` verwenden, was das intern erledigt.
info = credentials.get_placeholder("example")
Schlüssel/Wert-Speicher
Auf das Particle beschränkt. Nur Zeichenketten – base64-codiere Binärdaten bei Bedarf. Deklariere es mit kv=KV(enabled=True) im Particle – ohne die Deklaration löst jeder Aufruf einen not-declared-Fehler aus (keine Benutzerzustimmung erforderlich, nur die Deklaration):
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] – Hinweis: überschattet das Builtin
Importiere das Modul als kv (nicht from particle.kv import list), damit list das Builtin nicht überschattet.
OAuth-Aktualisierung
Nur, wenn du eine OAuth2-Anmeldedaten hast. Aufrufe von http.fetch mit credential_name= aktualisieren abgelaufene Token automatisch; rufe dies nur auf, wenn ein Upstream einen Token ablehnt, den du für noch gültig gehalten hast.
from particle import oauth
oauth.refresh("example")
Signierung
Nur für SigningKey-Anmeldedaten. Das Schlüsselmaterial gelangt nie in Python.
from particle import signing
sig = signing.sign("webhook-key", payload_bytes)
ok = signing.verify("webhook-key", data_bytes, signature_bytes)
4. Anmeldedaten-Methoden
Jede Credential hat eine methods={...}-Map. Der Benutzer wählt bei der Einrichtung eine aus. Die Dataclass-Typen stammen aus particle.manifest.
OAuth2
"oauth": OAuth2(
description="Über OAuth anmelden",
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", # für "device-code"
),
Gültige Flow-Strings: "authorization-code", "authorization-code-pkce", "device-code".
ApiKey
"pat": ApiKey(
description="Persönlichen Zugriffstoken verwenden",
location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),
ApiKeyLocation.kind ist einer von:
"header"+name:<name>: <key>"auth-scheme"+scheme:Authorization: <scheme> <key>(z.B.Bearer)"query-param"+name: angehängt als?<name>=<key>
Lasse location ganz weg, um den Importeur bei der Einrichtung zur Eingabe aufzufordern.
Basic
"basic_auth": Basic(description="Benutzername + Passwort"),
Ersetzt als Authorization: Basic <base64>.
SigningKey
"webhook": SigningKey(description="...", algorithm="hmac-sha256"),
Gültig: "hmac-sha256", "hmac-sha512". Verwende über signing.sign.
Raw
"api_key_blob": Raw(description="..."),
Verwende über credentials.get_raw(name). Wähle dies nur, wenn du wirklich die Rohbytes benötigst – die anderen Typen sind sicherer, da der Wert nie in den Python-Speicher gelangt.
5. PyPI-Abhängigkeiten
Deklariere im PEP-723-Block am Dateianfang:
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx>=0.27",
# "pyjwt>=2",
# ]
# ///
Einschränkungen:
- Nur Pure-Python-Wheels. Alles, was eine C-Erweiterung benötigt (numpy, pydantic v1, lxml, ...), wird nicht geladen.
- Die Standardbibliothek ist weitgehend verfügbar –
json,re,hashlib,hmac,base64,secrets,datetime,urllib.parse,asyncio,emailusw. funktionieren alle. ssl, rohessocket,subprocess– nicht verfügbar.open()/pathlibfunktionieren nur für Pfade innerhalb eines deklarierten Dateisystem-Mounts (siehe „Dateisystem“ oben) und des eigenen/particle/-Bundles des Particles; beliebige Host-Pfade werden verweigert.
6. Kritische Regeln
-
Keine Host-Aufrufe auf Modulebene. Aufrufe von
http.fetch,kv.get,credentials.get_placeholderusw. am Dateianfang schlagen zur Build-Zeit fehl (die Introspectionsphase führt dein Modul unter Trap-Stores aus). Führe alle Host-Aufrufe innerhalb einerhandler- oderping-Funktion durch.# ✗ Wird den Build zum Scheitern bringen _cached_data = http.fetch("https://api.example.com/init") particle = Particle(...) # ✓ Innerhalb des Handlers def _go(args): res = http.fetch("https://api.example.com/...") ... particle = Particle(tools={"go": Tool(..., handler=_go)}) -
Deklariere jeden Host, den du abfragst. Eine Anfrage an einen Host, der nicht in
http=Http(allowed_hosts=[...])enthalten ist, wird abgelehnt. Der Fehler wird als Ausnahme im Handler sichtbar. -
Credential.hostsmuss eine Teilmenge der HTTP-Erlaubnisliste sein. Der Build lehnt Anmeldedaten ab, die an Hosts gebunden sind, die das Particle nicht erreichen kann. -
namemuss kebab-case sein (Kleinbuchstaben, Bindestriche). Der Registrierungsschlüssel ist(name, version). -
versionmuss gültiges semver sein.0.1.0,1.2.3-rc.1usw. -
Tool-Handler müssen JSON-serialisierbare Werte zurückgeben. Einfaches
dict,list,str,int,float,bool,None. Ein Dataclass oderdatetimekann nicht serialisiert werden – konvertiere vor der Rückgabe.
7. Workflow
Nach dem Schreiben von Particlefile.py:
particle build # parsen, Abhängigkeiten auflösen, inspizieren, registrieren
particle ping <name> # Ping überprüfen (falls definiert)
particle run <name> # Tools auflisten
particle run <name> <tool> --help # Flags des Tools anzeigen
particle run <name> <tool> --foo=bar # es aufrufen
particle build führt den Benutzer beim ersten Mal interaktiv durch die Einrichtung der Anmeldedaten. Wenn der Build fehlschlägt, benennt der Fehler die Phase (import-scan, resolve-and-fetch, manifest-extract) und das spezifische Problem.
Wenn das Particle Dateisystem-Mounts deklariert, ordne sie vor dem Ausführen zu (oder übergib --mount name=path bei particle run für einen einmaligen Lauf):
particle mount <name> <mount-name> <host-pfad> # dauerhafte Zuordnung speichern
particle mount <name> # Mounts + aktuelle Zuordnungen auflisten
Um das Particle als eigenständige ausführbare Datei verfügbar zu machen (praktisch für Claude Code Skills und Shell-Nutzung), schreibt particle link <name> ./<name> einen Starter, der seine Argumente an particle run <name> weiterleitet.
8. Beispiel
Ein minimales Particle, das ein GitHub-Repo-Suchtool bereitstellt:
# /// 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_count"),
"description": r.get("description"),
}
particle = Particle(
name="github-repo-lookup",
description="Sucht GitHub-Repo-Informationen ab",
version="0.1.0",
http=Http(allowed_hosts=["api.github.com"]),
credentials={
"github": Credential(
hosts=["api.github.com"],
required=True,
methods={
"pat": ApiKey(
description="GitHub Personal Access Token",
location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),
},
),
},
tools={
"get_repo": Tool(
description="Ruft Informationen zu einem öffentlichen GitHub-Repository ab.",
input_schema={
"type": "object",
"properties": {
"owner": {"type": "string", "description": "Repository-Besitzer (Benutzer oder Organisation)"},
"repo": {"type": "string", "description": "Repository-Name"},
},
"required": ["owner", "repo"],
},
handler=_get_repo,
),
},
)
Erstelle die Datei, führe particle build aus und rufe das Tool auf:
particle build
particle run github-repo-lookup get_repo --owner=psf --repo=requests