Community코딩 & 개발github.com

partite-ai/write-particle-python

Python으로 Partite Particle을 생성, 편집 또는 확장합니다. Particlefile.py를 사용하여 Python 도구 핸들러, API 통합, 자격 증명 및 런타임 기능을 구현합니다. TypeScript 또는 JavaScript Particle의 경우 해당 언어 스킬을 사용하세요.

write-particle-python란 무엇인가요?

이 스킬은 Partite Particle을 Python으로 작성하는 방법을 상세히 설명합니다. Particle은 단일 파일(Particlefile.py)로 정의되며, 하나 이상의 도구(JSON-Schema 입력을 가진 명명된 작업)를 외부 런타임에 노출합니다. 사용자는 PEP 723 인라인 메타데이터 블록을 통해 PyPI 의존성을 선언하고, `from particle import ...` 모듈을 사용하여 아웃바운드 HTTP 호출, 파일시스템 접근, 자격 증명 관리, 키-값 저장소, OAuth 갱신 및 서명과 같은 모든 런타임 기능을 활용할 수 있습니다. 이 스킬은 필수 필드(이름, 설명, 버전)와 선택적 기능(HTTP 허용 목록, 자격 증명, 파일시스템 마운트, 핑)을 포함한 `Particle` 객체 생성 방법을 안내합니다. 또한 모듈 수준에서 호스트 호출을 피하는 것과 같은 중요한 규칙을 강조하며, 도구 핸들러가 JSON 직렬화 가능한 값을 반환해야 함을 명시합니다. 이 스킬은 Claude Code, Cursor, Codex 및 기타 Python 커스텀 도구를 지원하는 모든 에이전트 플랫폼에 유용합니다.

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

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Python Particle 작성하기

Particle은 하나 이상의 도구(JSON-Schema 입력을 가진 명명된 작업)를 Particle 런타임에 노출하는 단일 파일 프로그램입니다. 이 스킬은 빌드되고 실행되는 Particle을 만드는 데 필요한 모든 것을 알려줍니다.

1. 파일 구조

프로젝트 루트에 Particlefile.py라는 이름의 소스 파일 하나만 있으면 됩니다. 다른 것은 필요하지 않습니다. PyPI 의존성은 파일 상단의 PEP 723 인라인 메타데이터 블록에 선언되며, 빌드 시 해결됩니다. pyproject.toml이나 가상 환경을 관리할 필요가 없습니다.

# /// script
# requires-python = ">=3.12"
# dependencies = [
#   "idna>=3",       # 선택 사항 – 타사 패키지를 사용하는 경우에만
# ]
# ///

2. 모듈 수준의 particle — 전체 API

파일은 Particle 인스턴스를 모듈 수준의 particle 변수에 할당해야 합니다. 런타임은 빌드 시 이를 분석하고, 모든 도구 호출 시 핸들러를 읽어옵니다.

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",       # 필수, 케밥-케이스
    description="...",             # 필수, 한 줄
    version="0.1.0",               # 필수, 시맨틱 버전

    # 아웃바운드 HTTP 허용 목록. 여기에 없는 호스트는 네트워크 경계에서 차단됩니다.
    # Particle이 아웃바운드 요청을 전혀 하지 않으면 `http=`를 생략하세요.
    http=Http(allowed_hosts=["api.example.com"]),

    # 호스트 디렉토리 접근, 기본적으로 비활성화. 아래 "파일시스템" 참조.
    # filesystem=Filesystem(mounts={...}, temp={...}),

    # 선택 사항. 이름별로 선언하며, 사용자는 설정 시 방법을 선택합니다.
    credentials={
        "example": Credential(
            hosts=["api.example.com"],
            required=True,
            methods={
                # 아래 "자격 증명 방법" 참조
            },
        ),
    },

    # 필수. 이름 → Tool(설명, input_schema, 핸들러).
    tools={
        "do_thing": Tool(
            description="이 도구가 하는 일 – 한 줄, LLM용으로 작성.",
            input_schema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "..."},
                },
                "required": ["name"],
            },
            handler=_do_thing,
        ),
    },

    # 선택 사항. `particle ping`에 의해 호출됩니다.
    ping=_ping,
)

도구

  • description: LLM 호출자를 위해 작성 – 짧고, 행동 지향적입니다.
  • input_schema: JSON Schema Draft 2020-12, 객체 기반. 호스트는 핸들러를 호출하기 전에 인수를 검증하므로, 내부에서 딕셔너리 형태를 신뢰할 수 있습니다.
  • handler: 하나의 위치 딕셔너리를 받고 JSON 직렬화 가능한 값을 반환하는 일반 호출 가능 객체입니다. def 또는 async def일 수 있으며, 비동기 핸들러는 일회성 폴 루프에서 실행됩니다. 오류를 표시하려면 예외를 발생시키세요.

핑

선택 사항. 다음과 같은 딕셔너리를 반환하세요:

{"status": "ok" | "degraded" | "unhealthy",
 "message": "...",            # 선택 사항
 "details": ...}              # 선택 사항, JSON 직렬화 가능

3. 핸들러에서 사용 가능한 런타임 API

모든 호스트 기능은 from particle import ... 아래에 있습니다. 이것들이 필요한 유일한 비표준 라이브러리 모듈입니다.

아웃바운드 HTTP

HTTP 호출을 위한 네 가지 동등한 방법이 있습니다. 코드의 나머지 부분과 일치하는 관용구를 선택하세요. 모두 호스트 허용 목록을 통해 라우팅됩니다.

particle.http.fetch — 네이티브 API. 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="example",   # 선택 사항; 호스트가 시크릿을 대체합니다.
)
if not res.ok:
    raise RuntimeError(f"upstream {res.status_code}")
data = res.json()

http.fetch는 다음 속성을 가진 Response를 반환합니다:

  • .status_code (int), .ok (bool),
  • .headers ((name, bytes) 튜플의 리스트), .header(name) 조회,
  • .body (bytes), .text (str), .json() (파싱됨).

urllib.request.urlopen — 그대로 작동합니다. 런타임이 wasi:http를 통해 라우팅하도록 몽키-패치합니다.

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

urllib3 / requests — 그대로 작동합니다. 런타임이 urllib3 임포트 시점에 사용자 정의 연결 클래스를 주입하므로 requests.get(...) 등이 작동합니다. 자격 증명을 자동으로 적용하지 않습니다 — 인증된 요청의 경우 http.fetch(..., credential_name=...)를 사용하거나 플레이스홀더를 가져와 직접 헤더를 설정하세요.

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

httpx — 동기와 비동기 모두 그대로 작동합니다. 임포트 시 사용자 정의 HTTPTransport / AsyncHTTPTransport가 주입됩니다.

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

Raw 소켓은 사용할 수 없습니다. socket.socket()은 OSError를 발생시킵니다.

파일시스템

기본적으로 비활성화 – 핸들러는 자체 번들만 볼 수 있습니다. Filesystem 기능을 선언하여 호스트 디렉토리 접근 권한을 얻은 다음, 일반 open() / pathlib로 읽고 쓰세요(런타임이 wasi:filesystem을 통해 라우팅합니다).

from particle.manifest import Filesystem, Mount, TempMount

filesystem=Filesystem(
    # *사용자*가 실제 경로에 매핑하는 호스트 디렉토리 – `particle mount <particle> <name> <host-path>`로 영구적으로,
    # 또는 `--mount <name>=<host-path>`로 실행 시마다 매핑합니다.
    mounts={
        "data": Mount(
            description="보고서가 작성되는 위치",  # 설치 프롬프트에 표시됨
            path="/mnt/data",                          # 샌드박스 내부의 절대 경로
            access="readwrite",                        # "readonly" | "readwrite"
            required=True,                             # 매핑될 때까지 실행 거부 (기본값 False)
        ),
    },
    # 호스트가 각 실행 시 새로 프로비저닝하고 종료 시 지우는 임시 공간입니다. 항상 읽기-쓰기; 사용자는 매핑하지 않습니다.
    temp={
        "work": TempMount(description="임시 공간", 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)
  • 핸들러는 선언된 path를 직접 엽니다. 마운트 이름은 샌드박스 내부에 나타나지 않습니다.
  • readonly 마운트에 쓰거나, 선언된 마운트 외부의 경로를 읽거나 쓰면 OSError가 발생합니다.
  • 사용자가 매핑하지 않은 required 마운트는 빌드/설치 시가 아닌 런타임 시에 실패합니다.
  • max_size는 선택적 KB/MB/GB 접미사("10MB")가 있는 바이트 수이며, 이를 초과하는 쓰기는 실패합니다.

자격 증명

from particle import credentials

# 사용자가 설정 시 선택한 방법("oauth", "pat", ...) 또는 None.
method = credentials.get_configured_method("example")

# 유형-"raw" 자격 증명의 원시 값.
value = credentials.get_raw("some-raw-cred")

# 저수준: 불투명 플레이스홀더 + 호스트가 대체할 위치를 설명하는 적용-사양을 가져옵니다.
# 거의 항상 `http.fetch(..., credential_name=...)`를 대신 사용하고 싶을 것입니다.
# 이 함수는 내부적으로 이 작업을 수행합니다.
info = credentials.get_placeholder("example")

키/값 저장소

Particle별로 범위가 지정됩니다. 문자열만 가능 – 필요한 경우 바이너리를 base64로 인코딩하세요. Particle에 kv=KV(enabled=True)로 선언하세요 – 선언 없이 모든 호출은 not-declared 오류를 발생시킵니다(사용자 승인은 필요하지 않으며, 선언만 있으면 됩니다):

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]   — 참고: 내장 함수 shadows

list가 내장 함수를 shadow하지 않도록 모듈을 kv( from particle.kv import list가 아님)로 임포트하세요.

OAuth 갱신

OAuth2 자격 증명이 있는 경우에만 해당됩니다. credential_name=과 함께 http.fetch를 호출하면 만료된 토큰을 자동으로 갱신합니다. 이 함수는 여전히 유효하다고 생각했던 토큰을 업스트림이 거부할 때만 호출하세요.

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

서명

SigningKey 자격 증명에만 해당됩니다. 키 자료는 Python에 절대 들어오지 않습니다.

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

4. 자격 증명 방법

각 Credential에는 methods={...} 맵이 있습니다. 사용자는 설정 시 하나를 선택합니다. 데이터클래스 유형은 particle.manifest에서 가져옵니다.

OAuth2

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

유효한 플로우 문자열: "authorization-code", "authorization-code-pkce", "device-code".

ApiKey

"pat": ApiKey(
    description="개인 액세스 토큰 사용",
    location=ApiKeyLocation(kind="auth-scheme", scheme="Bearer"),
),

ApiKeyLocation.kind는 다음 중 하나입니다:

  • "header" + name: <name>: <key>
  • "auth-scheme" + scheme: Authorization: <scheme> <key> (예: Bearer)
  • "query-param" + name: ?<name>=<key>로 추가됨

location을 완전히 생략하면 임포터가 설정 시 사용자에게 프롬프트를 표시합니다.

Basic

"basic_auth": Basic(description="사용자 이름 + 비밀번호"),

Authorization: Basic <base64>로 대체됩니다.

SigningKey

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

유효: "hmac-sha256", "hmac-sha512". signing.sign을 통해 사용합니다.

Raw

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

credentials.get_raw(name)을 통해 사용합니다. 원시 바이트가 실제로 필요한 경우에만 이 방법을 선택하세요. 다른 유형은 값이 Python 메모리에 절대 들어오지 않기 때문에 더 안전합니다.

5. PyPI 의존성

파일 상단의 PEP 723 블록에 선언합니다:

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

제약 사항:

  • 순수 Python 휠만 가능합니다. C 확장이 필요한 것(numpy, pydantic v1, lxml 등)은 로드되지 않습니다.
  • 표준 라이브러리는 대부분 사용 가능합니다 – json, re, hashlib, hmac, base64, secrets, datetime, urllib.parse, asyncio, email 등 모두 작동합니다.
  • ssl, raw socket, subprocess – 사용할 수 없습니다.
  • open() / pathlib는 선언된 파일시스템 마운트(위 "파일시스템" 참조) 내부의 경로와 Particle 자체의 /particle/ 번들에 대해서만 작동합니다. 임의의 호스트 경로는 거부됩니다.

6. 중요 규칙

  1. 모듈 범위에서 호스트 호출 금지. 파일 상단에서 http.fetch, kv.get, credentials.get_placeholder 등을 호출하면 빌드 시 실패합니다(분석 단계가 트랩 저장소에서 모듈을 실행합니다). 모든 호스트 호출은 handler 또는 ping 함수 내부에서 수행하세요.

    # ✗ 빌드 실패
    _cached_data = http.fetch("https://api.example.com/init")
    particle = Particle(...)
    
    # ✓ 핸들러 내부에서
    def _go(args):
        res = http.fetch("https://api.example.com/...")
        ...
    particle = Particle(tools={"go": Tool(..., handler=_go)})
    
  2. 가져오는 모든 호스트를 선언하세요. http=Http(allowed_hosts=[...])에 없는 호스트에 대한 요청은 거부됩니다. 오류는 핸들러에서 예외로 표시됩니다.

  3. Credential.hosts는 HTTP 허용 목록의 하위 집합이어야 합니다. 빌드는 Particle이 연결할 수 없는 호스트에 바인딩된 자격 증명을 거부합니다.

  4. name은 케밥-케이스여야 합니다 (소문자, 하이픈). 레지스트리 키는 (name, version)입니다.

  5. version은 유효한 시맨틱 버전이어야 합니다. 0.1.0, 1.2.3-rc.1 등.

  6. 도구 핸들러는 JSON 직렬화 가능한 값을 반환해야 합니다. 일반 dict, list, str, int, float, bool, None. 데이터클래스 또는 datetime은 직렬화에 실패합니다 – 반환하기 전에 변환하세요.

7. 워크플로우

Particlefile.py를 작성한 후:

particle build                          # 파싱, 의존성 해결, 분석, 등록
particle ping <name>                    # ping 확인 (정의된 경우)
particle run  <name>                    # 도구 목록 표시
particle run  <name> <tool> --help      # 도구의 플래그 표시
particle run  <name> <tool> --foo=bar   # 호출

particle build는 처음에 대화식으로 자격 증명 설정을 안내합니다. 빌드가 실패하면 오류는 단계(import-scan, resolve-and-fetch, manifest-extract)와 특정 문제를 명명합니다.

Particle이 파일시스템 마운트를 선언하는 경우, 실행하기 전에 매핑하거나(또는 일회성으로 particle run에 --mount name=path 전달):

particle mount <name> <mount-name> <host-path>   # 영구 매핑 저장
particle mount <name>                            # 마운트 + 현재 매핑 목록 표시

Particle을 독립 실행형 실행 파일로 노출하려면(Claude Code 스킬 및 셸 사용에 편리), particle link <name> ./<name>은 인수를 particle run <name>에 전달하는 런처를 작성합니다.

8. 작업 예제

GitHub 저장소 조회 도구를 노출하는 최소 Particle:

# /// 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"),
    }

particle = Particle(
    name="github-repo",
    description="Look up a GitHub repo by owner and name",
    version="0.1.0",
    http=Http(allowed_hosts=["api.github.com"]),
    credentials={
        "github": Credential(
            hosts=["api.github.com"],
            required=True,
            methods={
                "token": ApiKey(
                    description="GitHub Personal Access Token",
                    location=ApiKeyLocation(kind="header", name="Authorization"),
                ),
            },
        ),
    },
    tools={
        "get_repo": Tool(
            description="Fetch repository metadata from GitHub",
            input_schema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string", "description": "Repository owner (user or org)"},
                    "repo":  {"type": "string", "description": "Repository name"},
                },
                "required": ["owner", "repo"],
            },
            handler=_get_repo,
        ),
    },
)

Individual skills in this repo

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

관련 스킬