partite-ai/write-particle-python

Crie, edite ou estenda uma Partícula Partite em Python usando Particlefile.py. Utilize para manipuladores de ferramentas Python, integrações de API, credenciais e capacidades de tempo de execução. Para Partículas em TypeScript ou JavaScript, use a habilidade da linguagem correspondente, quando disponível.

O que é write-particle-python?

Esta habilidade ensina como criar uma 'partícula' (particle) Partite em Python, que é um programa de arquivo único expondo uma ou mais ferramentas (operações nomeadas com entrada JSON-Schema) a um runtime de partículas. O arquivo, chamado exatamente `Particlefile.py`, usa um bloco de metadados inline PEP 723 para declarar dependências PyPI sem necessidade de `pyproject.toml` ou virtualenv. A habilidade detalha a API completa: um objeto `Particle` no nível do módulo contendo ferramentas com descrições e esquemas, capacidade de ping opcional, acesso HTTP de saída com lista de permissões, acesso ao sistema de arquivos do host via montagens (desligado por padrão), armazenamento chave/valor, credenciais (OAuth2, ApiKey, Basic, SigningKey, Raw), e refresh OAuth. Ela suporta múltiplas bibliotecas HTTP (`particle.http.fetch`, `urllib.request`, `urllib3`, `requests`, `httpx`) e inclui regras críticas como não fazer chamadas de host no escopo do módulo e declarar todos os hosts permitidos. A habilidade é útil para desenvolvedores que precisam criar ferramentas Python portáteis e seguras para integrações de API e automação, funcionando nos ecossistemas Partite, Claude Code, Cursor e Codex.

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

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Escreva uma partícula Python

Uma partícula é um programa de arquivo único que expõe uma ou mais ferramentas (operações nomeadas com entrada JSON-Schema) a um runtime de partículas. Esta habilidade fornece tudo que você precisa para produzir uma que compile e execute.

1. Estrutura do arquivo

Um arquivo fonte na raiz do projeto, nomeado exatamente Particlefile.py. Nada mais é necessário. As dependências PyPI são declaradas em um bloco inline de metadados PEP 723 no topo do arquivo; a construção as resolve. Sem pyproject.toml, sem virtualenv para gerenciar.

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "idna>=3",       # opcional — apenas se usar pacotes de terceiros
# ]
# ///

2. O particle no nível do módulo — a API completa

O arquivo deve atribuir uma instância de Particle a uma variável particle no nível do módulo. O runtime a inspeciona na construção, e lê os manipuladores a cada chamada de ferramenta.

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"olá {args['name']}"}

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

particle = Particle(
    name="nome-em-kebab-case",       # obrigatório, kebab-case
    description="...",                # obrigatório, uma linha
    version="0.1.0",                  # obrigatório, semver

    # Lista de permissões de HTTP de saída. Hosts não listados aqui são negados no
    # limite da rede. Omita `http=` se a partícula não fizer
    # nenhuma requisição de saída.
    http=Http(allowed_hosts=["api.example.com"]),

    # Acesso ao diretório do host, desligado por padrão. Veja "Sistema de arquivos" abaixo.
    # filesystem=Filesystem(mounts={...}, temp={...}),

    # Opcional. Declarado por nome; o usuário escolhe um método na configuração.
    credentials={
        "exemplo": Credential(
            hosts=["api.example.com"],
            required=True,
            methods={
                # veja "Métodos de credencial" abaixo
            },
        ),
    },

    # Obrigatório. nome → Tool(descrição, esquema_de_entrada, manipulador).
    tools={
        "fazer_coisa": Tool(
            description="O que esta ferramenta faz — uma linha, escrita para um LLM.",
            input_schema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "..."},
                },
                "required": ["name"],
            },
            handler=_do_thing,
        ),
    },

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

Ferramentas

  • description: escrita para um chamador LLM — curta, orientada à ação.
  • input_schema: JSON Schema Draft 2020-12, enraizado em objeto. O host valida os argumentos contra ele antes de chamar seu manipulador, então você pode confiar na forma do dict internamente.
  • handler: um callable simples que recebe um dict posicional e retorna qualquer valor serializável em JSON. Pode ser def ou async def; manipuladores assíncronos executam em um loop de polling único. Levante para expor um erro.

Ping

Opcional. Retorne um dict como:

{"status": "ok" | "degraded" | "unhealthy",
 "message": "...",            # opcional
 "details": ...}               # opcional, serializável em JSON

3. APIs de runtime disponíveis para manipuladores

Todas as capacidades do host estão sob from particle import .... Estes são os únicos módulos não padrão que você precisa.

HTTP de saída

Quatro maneiras equivalentes de fazer uma chamada HTTP. Escolha aquela que corresponde ao idioma que o resto do seu código usa; todas passam pela lista de permissões do host.

particle.http.fetch — a API nativa. Aplica automaticamente uma credencial quando credential_name= está definido.

from particle import http

res = http.fetch(
    "https://api.example.com/coisas",
    method="POST",
    headers={"Content-Type": "application/json"},
    body=json.dumps({...}).encode(),
    credential_name="exemplo",   # opcional; o host substitui o segredo
)
if not res.ok:
    raise RuntimeError(f"upstream {res.status_code}")
data = res.json()

http.fetch retorna uma Response com:

  • .status_code (int), .ok (bool),
  • .headers (lista de tuplas (nome, bytes)), .header(nome) consulta,
  • .body (bytes), .text (str), .json() (analisado).

urllib.request.urlopen — funciona como está. O runtime monkey-patcha ele para rotear através de wasi:http.

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

urllib3 / requests — funcionam como estão. O runtime injeta uma classe de conexão personalizada no momento da importação do urllib3, então requests.get(...) etc. simplesmente funcionam. Eles não aplicam credenciais automaticamente — para requisições autenticadas, use http.fetch(..., credential_name=...) ou puxe o placeholder e defina o cabeçalho você mesmo.

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

httpx — funciona como está, tanto síncrono quanto assíncrono. HTTPTransport / AsyncHTTPTransport personalizado injetado na importação.

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

Sockets brutos não estão disponíveis. socket.socket() levanta OSError.

Sistema de arquivos

Desligado por padrão — um manipulador vê apenas seu próprio pacote. Declare uma capacidade Filesystem para obter acesso ao diretório do host, depois leia e escreva com open() / pathlib comuns (o runtime os roteia através de wasi:filesystem).

from particle.manifest import Filesystem, Mount, TempMount

filesystem=Filesystem(
    # Diretórios do host que o *usuário* mapeia para caminhos reais — persistentemente
    # com `particle mount <particle> <nome> <caminho-host>`, ou por execução
    # com `--mount <nome>=<caminho-host>`.
    mounts={
        "dados": Mount(
            description="Onde os relatórios são escritos",  # mostrado no prompt de instalação
            path="/mnt/dados",                               # caminho absoluto dentro do sandbox
            access="readwrite",                              # "readonly" | "readwrite"
            required=True,                                    # recusa executar até mapeado (padrão False)
        ),
    },
    # Espaço temporário que o host provisiona fresco a cada execução e limpa na
    # saída. Sempre leitura-escrita; o usuário nunca mapeia estes.
    temp={
        "trabalho": TempMount(description="Espaço temporário", path="/tmp/trabalho", max_size="10MB"),
    },
),
with open("/mnt/dados/in.json") as f:
    data = f.read()
with open("/mnt/dados/out.json", "w") as f:
    f.write(result)
  • O manipulador abre o path declarado diretamente; o nome da montagem nunca aparece dentro do sandbox.
  • Escrever em uma montagem readonly, ou ler/escrever qualquer caminho fora de uma montagem declarada, levanta OSError.
  • Uma montagem required que o usuário nunca mapeia falha em tempo de execução, não construção/instalação.
  • max_size é uma contagem de bytes com um sufixo opcional KB/MB/GB ("10MB"); escritas além disso falham.

Credenciais

from particle import credentials

# O método que o usuário escolheu na configuração ("oauth", "pat", ...) ou None.
method = credentials.get_configured_method("exemplo")

# Valor bruto de uma credencial do tipo "raw".
value = credentials.get_raw("alguma-cred-raw")

# Baixo nível: obter um placeholder opaco + especificação de aplicação descrevendo onde
# o host espera substituí-lo. Quase sempre você quer
# `http.fetch(..., credential_name=...)` em vez disso, que faz isso
# internamente.
info = credentials.get_placeholder("exemplo")

Armazenamento chave/valor

Escopo por partícula. Apenas strings — codifique base64 binário se necessário. Declare-o com kv=KV(enabled=True) no Particle — sem a declaração, cada chamada levanta um erro not-declared (nenhuma aprovação do usuário é necessária, apenas a declaração):

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("prefixo:")       # list[str]   — note: sombreia o builtin

Importe o módulo como kv (não from particle.kv import list) para que list não sombreie o builtin.

Refresh OAuth

Apenas se você tiver uma credencial OAuth2. Chamadas a http.fetch com credential_name= renovam automaticamente tokens expirados; chame isso apenas quando um upstream rejeitar um token que você pensava ser ainda válido.

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

Assinatura

Apenas para credenciais SigningKey. O material da chave nunca entra no Python.

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

4. Métodos de credencial

Cada Credential tem um mapa methods={...}. O usuário escolhe um na configuração. Os tipos dataclass vêm de particle.manifest.

OAuth2

"oauth": OAuth2(
    description="Entrar 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",  # para "device-code"
),

Strings de fluxo válidas: "authorization-code", "authorization-code-pkce", "device-code".

ApiKey

"pat": ApiKey(
    description="Usar um token de acesso pessoal",
    location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),

ApiKeyLocation.kind é um de:

  • "header" + name: <name>: <key>
  • "auth-scheme" + scheme: Authorization: <scheme> <key> (ex: Bearer)
  • "query-param" + name: anexado como ?<name>=<key>

Omita location completamente para deixar o importador solicitar ao usuário na configuração.

Basic

"basic_auth": Basic(description="Nome de usuário + senha"),

Substitui como Authorization: Basic <base64>.

SigningKey

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

Válido: "hmac-sha256", "hmac-sha512". Use via signing.sign.

Raw

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

Use via credentials.get_raw(nome). Escolha isso apenas quando você genuinamente precisar dos bytes brutos — os outros tipos são mais seguros porque o valor nunca entra na memória do Python.

5. Dependências PyPI

Declare no bloco PEP 723 no topo do arquivo:

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

Restrições:

  • Apenas wheels Pure-Python. Qualquer coisa que precise de uma extensão C (numpy, pydantic v1, lxml, ...) não carregará.
  • A biblioteca padrão está amplamente disponível — json, re, hashlib, hmac, base64, secrets, datetime, urllib.parse, asyncio, email, etc. todos funcionam.
  • ssl, socket bruto, subprocess — não disponíveis.
  • open() / pathlib funcionam apenas para caminhos dentro de uma montagem de sistema de arquivos declarada (veja "Sistema de arquivos" acima) e do próprio pacote /particle/ da partícula; caminhos arbitrários do host são negados.

6. Regras críticas

  1. Sem chamadas de host no escopo do módulo. Chamar http.fetch, kv.get, credentials.get_placeholder, etc. no topo do arquivo falhará no momento da construção (a fase de introspecção executa seu módulo sob armadilhas de armazenamento). Faça toda chamada de host dentro de uma função handler ou ping.

    # ✗ Falhará a construção
    _cached_data = http.fetch("https://api.example.com/init")
    particle = Particle(...)
    
    # ✓ Dentro do manipulador
    def _go(args):
        res = http.fetch("https://api.example.com/...")
        ...
    particle = Particle(tools={"go": Tool(..., handler=_go)})
    
  2. Declare todo host que você buscar. Uma requisição a um host ausente de http=Http(allowed_hosts=[...]) é negada. O erro surge como uma exceção no manipulador.

  3. Credential.hosts deve ser um subconjunto da lista de permissões HTTP. A construção rejeita credenciais vinculadas a hosts que a partícula não pode alcançar.

  4. name deve ser kebab-case (minúsculas, hífens). A chave do registro é (name, version).

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

  6. Manipuladores de ferramenta devem retornar valores serializáveis em JSON. dict, list, str, int, float, bool, None simples. Uma dataclass ou datetime falhará ao serializar — converta antes de retornar.

7. Fluxo de trabalho

Após escrever Particlefile.py:

particle build                          # analisar, resolver deps, inspecionar, registrar
particle ping <nome>                    # verificar ping (se definido)
particle run  <nome>                    # listar ferramentas
particle run  <nome> <ferramenta> --help      # mostrar flags da ferramenta
particle run  <nome> <ferramenta> --foo=bar   # invocá-la

particle build guia o usuário pela configuração de credenciais interativamente na primeira vez. Se a construção falhar, o erro nomeia a fase (import-scan, resolve-and-fetch, manifest-extract) e o problema específico.

Se a partícula declarar montagens de sistema de arquivos, mapeie-as antes de executar (ou passe --mount name=path em particle run para uma execução única):

particle mount <nome> <nome-montagem> <caminho-host>   # salvar um mapeamento persistente
particle mount <nome>                                    # listar montagens + mapeamentos atuais

Para expor a partícula como um executável independente (útil para habilidades Claude Code e uso em shell), particle link <nome> ./<nome> escreve um lançador que repassa seus argumentos para particle run <nome>.

8. Exemplo trabalhado

Uma partícula mínima expondo uma ferramenta de consulta de repositório 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"),
        "language":  r.get("language"),
        "private":   r.get("private"),
        "url":       r.get("html_url"),
    }

particle = Particle(
    name="github-repo-lookup",
    description="Busca informações de um repositório público do 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="Token de acesso pessoal do GitHub",
                    location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
                ),
            },
        ),
    },
    tools={
        "get_repo": Tool(
            description="Busca informações de um repositório público do GitHub por proprietário e nome.",
            input_schema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string", "description": "Proprietário do repositório"},
                    "repo":  {"type": "string", "description": "Nome do repositório"},
                },
                "required": ["owner", "repo"],
            },
            handler=_get_repo,
        ),
    },
)

Individual skills in this repo

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

Habilidades Relacionadas