CommunityProgramación y desarrollogithub.com

partite-ai/escribir-particula-python

Crear, editar o extender una Partícula Partite en Python utilizando Particlefile.py. Útil para manejadores de herramientas Python, integraciones de API, credenciales y capacidades de tiempo de ejecución. Para partículas en TypeScript o JavaScript, utiliza la habilidad del lenguaje correspondiente cuando esté disponible.

¿Qué es escribir-particula-python?

Esta habilidad define cómo crear una 'partícula' en Python para el ecosistema Partite. Una partícula es un programa autocontenido en un único archivo (Particlefile.py) que expone una o más herramientas (operaciones con entrada JSON-Schema) a un runtime de partículas. Utiliza PEP 723 para declarar dependencias sin necesidad de pyproject.toml o virtualenv, y el runtime de Partite se encarga de todo el ciclo de vida: compilación, resolución de dependencias, registro y ejecución. La habilidad soporta llamadas HTTP salientes (a través de http.fetch, urllib, requests, httpx o urllib3), acceso a sistema de archivos declarativo mediante montajes, almacenamiento clave-valor, manejo de credenciales (OAuth2, API Key, Basic, SigningKey, Raw) y capacidades de firma. Está diseñada para desarrolladores que desean crear extensiones modulares para plataformas como Claude Code, Cursor o Codex, permitiendo integraciones personalizadas con APIs externas, gestión de secretos y operaciones de E/S dentro de un entorno sandbox seguro.

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

Installed? Explore more Programación y desarrollo skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Escribir una partícula Python

Una partícula es un programa de un solo archivo que expone una o más herramientas (operaciones con nombre y una entrada JSON-Schema) a un runtime de partículas. Esta habilidad te dice todo lo que necesitas para producir una que compile y funcione.

1. Estructura del archivo

Un solo archivo fuente en la raíz del proyecto, llamado exactamente Particlefile.py. No se requiere nada más. Las dependencias PyPI se declaran en un bloque de metadatos en línea PEP 723 al inicio del archivo; la compilación las resuelve. No hay pyproject.toml, ni virtualenv que gestionar.

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "idna>=3",       # opcional — solo si usas paquetes de terceros
# ]
# ///

2. La variable particle a nivel de módulo — toda la API

El archivo debe asignar una instancia de Particle a una variable a nivel de módulo particle. El runtime la inspecciona durante la compilación y lee los manejadores en cada llamada de herramienta.

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 _hacer_algo(args):
    return {"resultado": f"hola {args['nombre']}"}

def _ping():
    return {"status": "ok", "message": "vivo"}

particle = Particle(
    name="nombre-en-kebab-case",       # requerido, kebab-case
    description="...",             # requerido, una línea
    version="0.1.0",               # requerido, semver

    # Lista blanca de HTTP saliente. Los hosts no listados aquí son denegados
    # en el límite de la red. Omite `http=` por completo si la partícula no
    # realiza solicitudes salientes.
    http=Http(allowed_hosts=["api.example.com"]),

    # Acceso a directorios del host, desactivado por defecto. Ver "Sistema de archivos" más abajo.
    # filesystem=Filesystem(mounts={...}, temp={...}),

    # Opcional. Declarado por nombre; el usuario elige un método en la configuración.
    credentials={
        "ejemplo": Credential(
            hosts=["api.example.com"],
            required=True,
            methods={
                # ver "Métodos de credencial" más abajo
            },
        ),
    },

    # Requerido. nombre → Tool(description, input_schema, handler).
    tools={
        "hacer_algo": Tool(
            description="Qué hace esta herramienta — una línea, escrita para un LLM.",
            input_schema={
                "type": "object",
                "properties": {
                    "nombre": {"type": "string", "description": "..."},
                },
                "required": ["nombre"],
            },
            handler=_hacer_algo,
        ),
    },

    # Opcional. Llamado por `particle ping`.
    ping=_ping,
)

Herramientas

  • description: escrita para un LLM que llama — corta, orientada a la acción.
  • input_schema: JSON Schema Draft 2020-12, con raíz de objeto. El host valida los argumentos contra él antes de llamar a tu manejador, así que puedes confiar en la forma del dict dentro.
  • handler: un callable simple que toma un dict posicional y devuelve cualquier valor serializable a JSON. Puede ser def o async def; los manejadores asíncronos se ejecutan en un bucle de sondeo único. Lanza una excepción para exponer un error.

Ping

Opcional. Devuelve un dict como:

{"status": "ok" | "degraded" | "unhealthy",
 "message": "...",            # opcional
 "details": ...}              # opcional, serializable a JSON

3. APIs de tiempo de ejecución disponibles para los manejadores

Todas las capacidades del host viven bajo from particle import .... Estos son los únicos módulos no estándar que necesitas.

HTTP saliente

Cuatro formas equivalentes de hacer una llamada HTTP. Elige la que coincida con el idioma que usa el resto de tu código; todas pasan por la lista blanca del host.

particle.http.fetch — la API nativa. Aplica automáticamente una credencial cuando se establece 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="ejemplo",   # opcional; el host sustituye el secreto
)
if not res.ok:
    raise RuntimeError(f"upstream {res.status_code}")
data = res.json()

http.fetch devuelve un Response con:

  • .status_code (int), .ok (bool),
  • .headers (lista de tuplas (name, bytes)), búsqueda .header(name),
  • .body (bytes), .text (str), .json() (parseado).

urllib.request.urlopen — funciona tal cual. El runtime lo monkey-patchea para enrutarlo a través de wasi:http.

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

urllib3 / requests — funciona tal cual. El runtime inyecta una clase de conexión personalizada en el momento de importar urllib3, por lo que requests.get(...) etc. funcionan. No aplican credenciales automáticamente — para solicitudes autenticadas, usa http.fetch(..., credential_name=...) o extrae el marcador de posición y establece el encabezado tú mismo.

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

httpx — funciona tal cual, tanto síncrono como asíncrono. Se inyecta un HTTPTransport / AsyncHTTPTransport personalizado en la importación.

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

Los sockets sin procesar no están disponibles. socket.socket() lanza OSError.

Sistema de archivos

Desactivado por defecto — un manejador solo ve su propio paquete. Declara una capacidad Filesystem para obtener acceso al directorio del host, luego lee y escribe con open() / pathlib normales (el runtime los enruta a través de wasi:filesystem).

from particle.manifest import Filesystem, Mount, TempMount

filesystem=Filesystem(
    # Directorios del host que el *usuario* asigna a rutas reales — de forma persistente
    # con `particle mount <particle> <nombre> <ruta-host>`, o por ejecución
    # con `--mount <nombre>=<ruta-host>`.
    mounts={
        "datos": Mount(
            description="Donde se escriben los informes",  # se muestra en el mensaje de instalación
            path="/mnt/datos",                          # ruta absoluta dentro del sandbox
            access="readwrite",                        # "readonly" | "readwrite"
            required=True,                             # rechaza ejecutarse hasta que se asigne (por defecto False)
        ),
    },
    # Espacio temporal que el host provisiona fresco cada ejecución y limpia al
    # salir. Siempre lectura-escritura; el usuario nunca los asigna.
    temp={
        "trabajo": TempMount(description="Espacio temporal", path="/tmp/trabajo", max_size="10MB"),
    },
),
with open("/mnt/datos/in.json") as f:
    data = f.read()
with open("/mnt/datos/out.json", "w") as f:
    f.write(result)
  • El manejador abre directamente la path declarada; el nombre del montaje nunca aparece dentro del sandbox.
  • Escribir en un montaje readonly, o leer/escribir cualquier ruta fuera de un montaje declarado, lanza OSError.
  • Un montaje required que el usuario nunca asigna falla en tiempo de ejecución, no en compilación/instalación.
  • max_size es un conteo de bytes con un sufijo opcional KB/MB/GB ("10MB"); las escrituras que lo superen fallan.

Credenciales

from particle import credentials

# El método que el usuario eligió en la configuración ("oauth", "pat", ...) o None.
method = credentials.get_configured_method("ejemplo")

# Valor sin procesar de una credencial de tipo "raw".
value = credentials.get_raw("alguna-cred-raw")

# Bajo nivel: obtener un marcador de posición opaco + especificación de aplicación que describe
# dónde el host espera sustituir el secreto. Casi siempre quieres
# `http.fetch(..., credential_name=...)` en su lugar, que hace esto
# internamente.
info = credentials.get_placeholder("ejemplo")

Almacenamiento clave/valor

Alcance por partícula. Solo cadenas — codifica en base64 binario si es necesario. Decláralo con kv=KV(enabled=True) en la Particle — sin la declaración, cada llamada lanza un error not-declared (no se necesita aprobación del usuario, solo la declaración):

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("prefijo:")       # list[str]   — nota: oculta el builtin

Importa el módulo como kv (no from particle.kv import list) para que list no oculte el builtin.

Actualización OAuth

Solo si tienes una credencial OAuth2. Las llamadas a http.fetch con credential_name= actualizan automáticamente tokens caducados; llama a esto solo cuando un upstream rechaza un token que pensaste que seguía siendo válido.

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

Firma

Solo para credenciales SigningKey. El material clave nunca entra en Python.

from particle import signing
sig = signing.sign("webhook-key", bytes_payload)
ok  = signing.verify("webhook-key", bytes_datos, bytes_firma)

4. Métodos de credencial

Cada Credential tiene un mapa methods={...}. El usuario elige uno en la configuración. Los tipos de dataclass provienen de particle.manifest.

OAuth2

"oauth": OAuth2(
    description="Iniciar sesión mediante 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",  # para "device-code"
),

Cadenas de flujo válidas: "authorization-code", "authorization-code-pkce", "device-code".

ApiKey

"pat": ApiKey(
    description="Usar un token de acceso personal",
    location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),

ApiKeyLocation.kind es uno de:

  • "header" + name: <name>: <key>
  • "auth-scheme" + scheme: Authorization: <scheme> <key> (ej. Bearer)
  • "query-param" + name: añadido como ?<name>=<key>

Omite location por completo para que el importador pregunte al usuario en la configuración.

Basic

"basic_auth": Basic(description="Usuario + contraseña"),

Se sustituye como Authorization: Basic <base64>.

SigningKey

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

Válidos: "hmac-sha256", "hmac-sha512". Se usa mediante signing.sign.

Raw

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

Se usa mediante credentials.get_raw(nombre). Elige esto solo cuando realmente necesites los bytes sin procesar — los otros tipos son más seguros porque el valor nunca entra en la memoria de Python.

5. Dependencias PyPI

Declara en el bloque PEP 723 al inicio del archivo:

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

Restricciones:

  • Solo ruedas Pure-Python. Cualquier cosa que necesite una extensión C (numpy, pydantic v1, lxml, ...) no se cargará.
  • La biblioteca estándar está en gran medida disponible — json, re, hashlib, hmac, base64, secrets, datetime, urllib.parse, asyncio, email, etc., todos funcionan.
  • ssl, socket sin procesar, subprocess — no disponibles.
  • open() / pathlib funcionan solo para rutas dentro de un montaje del sistema de archivos declarado (ver "Sistema de archivos" arriba) y el propio paquete /particle/ de la partícula; las rutas arbitrarias del host son denegadas.

6. Reglas críticas

  1. Sin llamadas al host en el ámbito del módulo. Llamar a http.fetch, kv.get, credentials.get_placeholder, etc. al inicio del archivo fallará en tiempo de compilación (la fase de introspección ejecuta tu módulo bajo trampas de almacenamiento). Haz cada llamada al host dentro de una función handler o ping.

    # ✗ Fallará la compilación
    _datos_cache = http.fetch("https://api.example.com/init")
    particle = Particle(...)
    
    # ✓ Dentro del manejador
    def _ir(args):
        res = http.fetch("https://api.example.com/...")
        ...
    particle = Particle(tools={"ir": Tool(..., handler=_ir)})
    
  2. Declara cada host al que llames. Una solicitud a un host que falte en http=Http(allowed_hosts=[...]) es denegada. El error se manifiesta como una excepción en el manejador.

  3. Credential.hosts debe ser un subconjunto de la lista blanca HTTP. La compilación rechaza credenciales vinculadas a hosts a los que la partícula no puede acceder.

  4. name debe ser kebab-case (minúsculas, guiones). La clave del registro es (name, version).

  5. version debe ser semver válido. 0.1.0, 1.2.3-rc.1, etc.

  6. Los manejadores de herramientas deben devolver valores serializables a JSON. dict, list, str, int, float, bool, None simples. Un dataclass o datetime fallará al serializarse — convierte antes de devolver.

7. Flujo de trabajo

Después de escribir Particlefile.py:

particle build                          # parse, resuelve dependencias, inspecciona, registra
particle ping <nombre>                  # verificar ping (si está definido)
particle run  <nombre>                  # listar herramientas
particle run  <nombre> <herramienta> --help      # mostrar banderas de la herramienta
particle run  <nombre> <herramienta> --foo=bar   # ejecutarla

particle build guía al usuario a través de la configuración de credenciales de forma interactiva la primera vez. Si la compilación falla, el error nombra la fase (import-scan, resolve-and-fetch, manifest-extract) y el problema específico.

Si la partícula declara montajes del sistema de archivos, asígnalos antes de ejecutar (o pasa --mount nombre=ruta en particle run para una ejecución única):

particle mount <nombre> <nombre-montaje> <ruta-host>   # guarda un mapeo persistente
particle mount <nombre>                            # listar montajes + mapeos actuales

Para exponer la partícula como un ejecutable independiente (útil para habilidades de Claude Code y uso en shell), particle link <nombre> ./<nombre> escribe un lanzador que reenvía sus argumentos a particle run <nombre>.

8. Ejemplo práctico

Una partícula mínima que expone una herramienta de consulta de repositorio de GitHub:

# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
import json
from particle import http
from particle.manifest import Particle, Tool, Http, Credential, ApiKey, ApiKeyLocation

def _obtener_repo(args):
    res = http.fetch(
        f"https://api.github.com/repos/{args['propietario']}/{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"),
        "estrellas": r.get("stargazers_count"),
        "descripción": r.get("description"),
        "url": r.get("html_url"),
    }

particle = Particle(
    name="consulta-github",
    description="Obtener detalles de un repositorio público de GitHub",
    version="0.1.0",
    http=Http(allowed_hosts=["api.github.com"]),
    credentials={
        "github": Credential(
            hosts=["api.github.com"],
            required=False,
            methods={
                "pat": ApiKey(
                    description="Token de acceso personal de GitHub",
                    location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
                ),
            },
        ),
    },
    tools={
        "consultar_repo": Tool(
            description="Obtener metadatos de un repositorio de GitHub",
            input_schema={
                "type": "object",
                "properties": {
                    "propietario": {"type": "string", "description": "Propietario del repositorio"},
                    "repo": {"type": "string", "description": "Nombre del repositorio"},
                },
                "required": ["propietario", "repo"],
            },
            handler=_obtener_repo,
        ),
    },
)

Compílalo y pruébalo:

particle build
particle run consulta-github consultar_repo --propietario=microsoft --repo=vscode

Individual skills in this repo

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

Skills relacionados