CommunityCodierung & Entwicklunggithub.com

partite-ai/write-particle-python

Erstelle, bearbeite oder erweitere ein Partite Particle in Python mit Particlefile.py. Verwendbar für Python-Tool-Handler, API-Integrationen, Anmeldedaten und Laufzeitfähigkeiten. Für TypeScript- oder JavaScript-Partikel verwende das entsprechende Sprach-Skill, falls verfügbar.

Was ist write-particle-python?

Dieses Skill beschreibt detailliert, wie man ein Python-Partikel (Particle) für die Partite-Runtime erstellt. Ein Partikel ist ein einzelnes Python-Skript (`Particlefile.py`), das eine oder mehrere Tools (benannte Operationen mit einem JSON-Schema-Input) bereitstellt. Das Skill erklärt die Dateistruktur, das API-Design mit der `Particle`-Klasse und ihren Optionen (HTTP, Dateisystem, Anmeldedaten, KV-Store, OAuth-Refresh, Signierung) sowie die nutzbaren Laufzeit-APIs. Es wird betont, dass der Code komplett in einer einzigen Datei liegt, dass Abhängigkeiten per PEP 723-Inline-Metadaten deklariert werden, und dass Host-Aufrufe nur innerhalb von Handlern erlaubt sind. Das Skill unterstützt die Partite-CLI und behandelt Workflows wie Build, Ping, Run, Mount und Link. Es ist ideal für Entwickler, die sichere, sandboxed Python-Tools für Partite erstellen möchten, und funktioniert plattformübergreifend mit gängigen HTTP-Bibliotheken (urllib, requests, httpx) sowie OAuth2, API-Keys und Dateisystemzugriff.

Funktioniert mit✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/partite-ai/partite-skills/tree/main/skills/write-particle-python

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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. Kann def oder async def sein; 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 path direkt; der Mount-Name erscheint nie innerhalb der Sandbox.
  • Schreiben auf einen readonly-Mount oder Lesen/Schreiben eines Pfades außerhalb eines deklarierten Mounts löst OSError aus.
  • Ein required-Mount, den der Benutzer nie zuordnet, schlägt zur Laufzeit fehl, nicht beim Build/Install.
  • max_size ist eine Byteanzahl mit einem optionalen Suffix KB/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, email usw. funktionieren alle.
  • ssl, rohes socket, subprocess – nicht verfügbar.
  • open() / pathlib funktionieren 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

  1. Keine Host-Aufrufe auf Modulebene. Aufrufe von http.fetch, kv.get, credentials.get_placeholder usw. am Dateianfang schlagen zur Build-Zeit fehl (die Introspectionsphase führt dein Modul unter Trap-Stores aus). Führe alle Host-Aufrufe innerhalb einer handler- oder ping-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)})
    
  2. 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.

  3. Credential.hosts muss eine Teilmenge der HTTP-Erlaubnisliste sein. Der Build lehnt Anmeldedaten ab, die an Hosts gebunden sind, die das Particle nicht erreichen kann.

  4. name muss kebab-case sein (Kleinbuchstaben, Bindestriche). Der Registrierungsschlüssel ist (name, version).

  5. version muss gültiges semver sein. 0.1.0, 1.2.3-rc.1 usw.

  6. Tool-Handler müssen JSON-serialisierbare Werte zurückgeben. Einfaches dict, list, str, int, float, bool, None. Ein Dataclass oder datetime kann 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

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Verwandte Skills