partite-ai/write-particle-python

Créez, modifiez ou étendez une Particle Python en utilisant Particlefile.py. Utilisé pour les gestionnaires d'outils Python, les intégrations API, les identifiants et les capacités d'exécution. Pour les Particles TypeScript ou JavaScript, utilisez la compétence linguistique correspondante lorsqu'elle est disponible.

Qu'est-ce que write-particle-python ?

Cette compétence guide la création d'un **Particle Python** : un programme monofichier exposant un ou plusieurs outils (opérations nommées avec un schéma JSON) à un runtime Particle. Elle détaille la structure du fichier `Particlefile.py` avec le bloc de métadonnées PEP 723, la définition de l'objet `Particle` avec ses outils, son `ping`, et ses capacités (`http`, `filesystem`, `credentials`, `kv`, `oauth`, `signing`). L'utilisateur apprend à déclarer les dépendances PyPI, configurer les permissions HTTP (liste blanche d'hôtes), gérer les credentials (OAuth2, ApiKey, Basic, SigningKey, Raw), et utiliser les API runtime comme `particle.http.fetch` ou `urllib.request`. Elle détaille les règles critiques (pas d'appels hôtes en dehors des handlers, déclaration obligatoire des hôtes, validation des credentials, format kebab-case, versions semver, valeurs sérialisables JSON). Enfin, elle présente le workflow CLI (`particle build`, `ping`, `run`, `mount`, `link`) et donne un exemple complet de Particle interrogeant GitHub. La compétence est essentielle pour tout développeur souhaitant créer des extensions Python robustes et sécurisées pour les environnements Particle, que ce soit pour Claude Code, Cursor ou d'autres agents.

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

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Écrire une Particle Python

Une particle est un programme monofichier qui expose un ou plusieurs outils (opérations nommées avec une entrée JSON-Schema) à un runtime Particle. Cette compétence vous apprend tout ce qu'il faut pour en produire une qui se compile et s'exécute.

1. Organisation du fichier

Un seul fichier source à la racine du projet, nommé exactement Particlefile.py. Rien d'autre n'est requis. Les dépendances PyPI sont déclarées dans un bloc de métadonnées en ligne PEP 723 en haut du fichier ; la compilation les résout. Pas de pyproject.toml, pas de virtualenv à gérer.

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "idna>=3",       # optionnel — uniquement si vous utilisez des packages tiers
# ]
# ///

2. particle au niveau du module — toute l'API

Le fichier doit assigner une instance de Particle à une variable particle au niveau du module. Le runtime l'inspecte à la compilation et lit les gestionnaires à chaque appel d'outil.

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",       # requis, en kebab-case
    description="...",             # requis, une ligne
    version="0.1.0",               # requis, semver

    # Liste blanche HTTP sortante. Les hôtes non listés ici sont refusés à
    # la limite du réseau. Omettez complètement `http=` si la particle
    # n'effectue aucune requête sortante.
    http=Http(allowed_hosts=["api.example.com"]),

    # Accès aux répertoires hôtes, désactivé par défaut. Voir "Système de
    # fichiers" ci-dessous.
    # filesystem=Filesystem(mounts={...}, temp={...}),

    # Optionnel. Déclaré par nom ; l'utilisateur choisit une méthode à
    # l'installation.
    credentials={
        "example": Credential(
            hosts=["api.example.com"],
            required=True,
            methods={
                # voir "Méthodes d'identifiants" ci-dessous
            },
        ),
    },

    # Requis. nom → Tool(description, input_schema, handler).
    tools={
        "do_thing": Tool(
            description="Ce que fait cet outil — une ligne, écrite pour un LLM.",
            input_schema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "..."},
                },
                "required": ["name"],
            },
            handler=_do_thing,
        ),
    },

    # Optionnel. Appelé par `particle ping`.
    ping=_ping,
)

Outils

  • description : écrite pour un appelant LLM — courte, orientée action.
  • input_schema : JSON Schema Draft 2020-12, ancré sur un objet. L'hôte valide les arguments par rapport à celui-ci avant d'appeler votre gestionnaire, vous pouvez donc faire confiance à la forme du dict à l'intérieur.
  • handler : un appelable simple prenant un dict positionnel et retournant n'importe quelle valeur sérialisable en JSON. Peut être def ou async def ; les gestionnaires asynchrones s'exécutent sur une boucle d'interrogation unique. Levez une exception pour signaler une erreur.

Ping

Optionnel. Retournez un dict comme :

{"status": "ok" | "degraded" | "unhealthy",
 "message": "...",            # optionnel
 "details": ...}              # optionnel, sérialisable en JSON

3. API runtime disponibles pour les gestionnaires

Toutes les capacités de l'hôte se trouvent sous from particle import .... Ce sont les seuls modules non-standard dont vous avez besoin.

HTTP sortant

Quatre façons équivalentes d'effectuer un appel HTTP. Choisissez celle qui correspond à l'idiome utilisé par le reste de votre code ; elles passent toutes par la liste blanche de l'hôte.

particle.http.fetch — l'API native. Applique automatiquement un identifiant lorsque credential_name= est défini.

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",   # optionnel ; l'hôte substitue le secret
)
if not res.ok:
    raise RuntimeError(f"upstream {res.status_code}")
data = res.json()

http.fetch retourne un Response avec :

  • .status_code (int), .ok (bool),
  • .headers (liste de tuples (name, bytes)), .header(name) pour la recherche,
  • .body (bytes), .text (str), .json() (analysé).

urllib.request.urlopen — fonctionne tel quel. Le runtime le monkey-patche pour le router via wasi:http.

import urllib.request
with urllib.request.urlopen("https://api.example.com/x") as r:
    body = r.read()

urllib3 / requests — fonctionnent tels quels. Le runtime injecte une classe de connexion personnalisée au moment de l'import de urllib3, donc requests.get(...) etc. fonctionnent. Ils n'appliquent pas les identifiants automatiquement — pour les requêtes authentifiées, utilisez soit http.fetch(..., credential_name=...) soit récupérez le placeholder et définissez l'en-tête vous-même.

import requests
r = requests.get("https://api.example.com/x")     # fonctionne

httpx — fonctionne tel quel, synchrone et asynchrone. HTTPTransport / AsyncHTTPTransport personnalisé injecté à l'import.

import httpx
r = httpx.get("https://api.example.com/x")

Les sockets bruts ne sont pas disponibles. socket.socket() lève OSError.

Système de fichiers

Désactivé par défaut — un gestionnaire ne voit que son propre bundle. Déclarez une capacité Filesystem pour obtenir l'accès au répertoire hôte, puis lisez et écrivez avec open() / pathlib ordinaires (le runtime les route via wasi:filesystem).

from particle.manifest import Filesystem, Mount, TempMount

filesystem=Filesystem(
    # Répertoires hôtes que *l'utilisateur* mappe vers des chemins réels — de
    # manière persistante avec `particle mount <particle> <nom> <chemin-hôte>`,
    # ou par exécution avec `--mount <nom>=<chemin-hôte>`.
    mounts={
        "data": Mount(
            description="Où les rapports sont écrits",  # affiché dans l'invite d'installation
            path="/mnt/data",                         # chemin absolu dans le sandbox
            access="readwrite",                       # "readonly" | "readwrite"
            required=True,                             # refuse de s'exécuter tant que non mappé (par défaut False)
        ),
    },
    # Espace temporaire que l'hôte provisionne fraîchement à chaque exécution
    # et efface à la sortie. Toujours en lecture-écriture ; l'utilisateur ne
    # mappe jamais ces chemins.
    temp={
        "work": TempMount(description="Espace temporaire", 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)
  • Le gestionnaire ouvre directement le path déclaré ; le nom du montage n'apparaît jamais dans le sandbox.
  • Écrire sur un montage readonly, ou lire/écrire tout chemin en dehors d'un montage déclaré, lève OSError.
  • Un montage required que l'utilisateur ne mappe jamais échoue au moment de l'exécution, pas à la compilation/installation.
  • max_size est un nombre d'octets avec un suffixe optionnel KB/MB/GB ("10MB") ; les écritures au-delà échouent.

Identifiants

from particle import credentials

# La méthode que l'utilisateur a choisie à l'installation ("oauth", "pat", ...) ou None.
method = credentials.get_configured_method("example")

# Valeur brute d'un identifiant de type "raw".
value = credentials.get_raw("some-raw-cred")

# Bas niveau : obtenir un placeholder opaque + une spécification d'application
# décrivant où l'hôte attend de substituer le secret. Presque toujours vous
# voulez `http.fetch(..., credential_name=...)` à la place, qui fait cela
# en interne.
info = credentials.get_placeholder("example")

Magasin clé/valeur

Limité à chaque particle. Chaînes uniquement — encodez en base64 les données binaires si nécessaire. Déclarez-le avec kv=KV(enabled=True) sur le Particle — sans la déclaration, chaque appel lève une erreur not-declared (aucune approbation utilisateur n'est nécessaire, juste la déclaration) :

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]   — note : masque le builtin

Importez le module en tant que kv (pas from particle.kv import list) pour que list ne masque pas le builtin.

Rafraîchissement OAuth

Uniquement si vous avez un identifiant OAuth2. Les appels à http.fetch avec credential_name= rafraîchissent automatiquement les jetons expirés ; appelez ceci uniquement lorsqu'un amont rejette un jeton que vous pensiez encore valide.

from particle import oauth
oauth.refresh("example")

Signature

Uniquement pour les identifiants SigningKey. Le matériel de clé n'entre jamais dans Python.

from particle import signing
sig = signing.sign("webhook-key", payload_bytes)
ok  = signing.verify("webhook-key", data_bytes, signature_bytes)

4. Méthodes d'identifiants

Chaque Credential a une carte methods={...}. L'utilisateur en choisit une à l'installation. Les types de dataclass proviennent de particle.manifest.

OAuth2

"oauth": OAuth2(
    description="Se connecter via 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",  # pour "device-code"
),

Chaînes de flux valides : "authorization-code", "authorization-code-pkce", "device-code".

ApiKey

"pat": ApiKey(
    description="Utiliser un jeton d'accès personnel",
    location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),

ApiKeyLocation.kind est l'un de :

  • "header" + name : <name>: <key>
  • "auth-scheme" + scheme : Authorization: <scheme> <key> (par ex. Bearer)
  • "query-param" + name : ajouté comme ?<name>=<key>

Omettez complètement location pour laisser l'importateur inviter l'utilisateur à l'installation.

Basic

"basic_auth": Basic(description="Nom d'utilisateur + mot de passe"),

Substitue comme Authorization: Basic <base64>.

SigningKey

"webhook": SigningKey(description="...", algorithm="hmac-sha256"),

Valides : "hmac-sha256", "hmac-sha512". Utilisez via signing.sign.

Raw

"api_key_blob": Raw(description="..."),

Utilisez via credentials.get_raw(name). Choisissez ceci uniquement lorsque vous avez vraiment besoin des octets bruts — les autres types sont plus sûrs car la valeur n'entre jamais dans la mémoire Python.

5. Dépendances PyPI

Déclarez dans le bloc PEP 723 en haut du fichier :

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "httpx>=0.27",
#   "pyjwt>=2",
# ]
# ///

Contraintes :

  • Wheels pure-Python uniquement. Tout ce qui nécessite une extension C (numpy, pydantic v1, lxml, ...) ne se chargera pas.
  • La bibliothèque standard est largement disponible — json, re, hashlib, hmac, base64, secrets, datetime, urllib.parse, asyncio, email, etc. fonctionnent tous.
  • ssl, socket brut, subprocess — pas disponibles.
  • open() / pathlib fonctionnent uniquement pour les chemins à l'intérieur d'un montage de système de fichiers déclaré (voir "Système de fichiers" ci-dessus) et du propre bundle /particle/ de la particle ; les chemins hôtes arbitraires sont refusés.

6. Règles critiques

  1. Pas d'appels hôtes au niveau du module. Appeler http.fetch, kv.get, credentials.get_placeholder, etc. en haut du fichier échouera au moment de la compilation (la phase d'inspection exécute votre module sous des magasins de pièges). Faites chaque appel hôte à l'intérieur d'une fonction handler ou ping.

    # ✗ Échouera la compilation
    _cached_data = http.fetch("https://api.example.com/init")
    particle = Particle(...)
    
    # ✓ À l'intérieur du gestionnaire
    def _go(args):
        res = http.fetch("https://api.example.com/...")
        ...
    particle = Particle(tools={"go": Tool(..., handler=_go)})
    
  2. Déclarez chaque hôte que vous appelez. Une requête vers un hôte manquant dans http=Http(allowed_hosts=[...]) est refusée. L'erreur se présente comme une exception dans le gestionnaire.

  3. Credential.hosts doit être un sous-ensemble de la liste blanche HTTP. La compilation rejette les identifiants liés à des hôtes que la particle ne peut pas atteindre.

  4. name doit être en kebab-case (minuscules, traits d'union). La clé du registre est (name, version).

  5. version doit être un semver valide. 0.1.0, 1.2.3-rc.1, etc.

  6. Les gestionnaires d'outils doivent retourner des valeurs sérialisables en JSON. dict, list, str, int, float, bool, None simples. Un dataclass ou datetime échouera à la sérialisation — convertissez avant de retourner.

7. Flux de travail

Après avoir écrit Particlefile.py :

particle build                          # analyser, résoudre les dépendances, inspecter, enregistrer
particle ping <name>                    # vérifier le ping (si défini)
particle run  <name>                    # lister les outils
particle run  <name> <tool> --help      # afficher les indicateurs de l'outil
particle run  <name> <tool> --foo=bar   # l'invoquer

particle build guide l'utilisateur à travers la configuration des identifiants de manière interactive la première fois. Si la compilation échoue, l'erreur nomme la phase (import-scan, resolve-and-fetch, manifest-extract) et le problème spécifique.

Si la particle déclare des montages de système de fichiers, mappez-les avant d'exécuter (ou passez --mount name=path sur particle run pour un usage unique) :

particle mount <name> <mount-name> <host-path>   # enregistrer un mappage persistant
particle mount <name>                            # lister les montages + mappages actuels

Pour exposer la particle en tant qu'exécutable autonome (pratique pour les compétences Claude Code et l'utilisation en ligne de commande), particle link <name> ./<name> écrit un lanceur qui transmet ses arguments à particle run <name>.

8. Exemple concret

Une particle minimale exposant un outil de recherche de dépôt 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_count"),
        "url":       r.get("html_url"),
        "description": r.get("description"),
    }

particle = Particle(
    name="github-lookup",
    description="Rechercher un dépôt GitHub",
    version="0.1.0",
    http=Http(allowed_hosts=["api.github.com"]),
    credentials={
        "github": Credential(
            hosts=["api.github.com"],
            required=True,
            methods={
                "pat": ApiKey(
                    description="Jeton d'accès personnel GitHub",
                    location=ApiKeyLocation(kind="header", name="Authorization"),
                ),
            },
        ),
    },
    tools={
        "lookup-repo": Tool(
            description="Obtenir les détails d'un dépôt GitHub par propriétaire et nom de dépôt.",
            input_schema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string", "description": "Propriétaire du dépôt (nom d'utilisateur ou organisation)"},
                    "repo": {"type": "string", "description": "Nom du dépôt"},
                },
                "required": ["owner", "repo"],
            },
            handler=_get_repo,
        ),
    },
)

Individual skills in this repo

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

Skills associés