Skip to content

partite-ai/particles

Repository files navigation

Particles

Particles lets you write small, self-contained, capability-sandboxed programs ("particles") that expose typed tools to an LLM. The common case: you want to plug a remote system into an LLM, but it has no MCP server, or it has one that dumps so much into the model's context that the rest of the conversation suffers. A particle ships just the handful of tools you actually want exposed, with hard isolation between the LLM-facing API and the credentials behind it — so you can wire a polished adapter into your harness instead of an off-the-shelf MCP you can't trim.

A particle is one ESM module: a manifest, a few tools, an explicit list of capabilities (HTTP hosts it can reach), and a declared set of credentials it needs. The same artifact works two ways: drop it into Claude Desktop / Cursor / any MCP client via particle serve-mcp, or call its tools straight from the shell with particle run — useful for Claude Code skills and ad-hoc scripts. Every particle also gets a per-particle key/value store out of the box — no declaration needed.

Six things make it different from "just a script":

  • Inputs validated before your handler runs. Each tool declares a JSON-Schema input. The runtime checks the call before your code sees it, so a missing or wrong-typed argument surfaces a clear error rather than a stack trace from inside the handler.
  • Sandboxed by capability. Particles run in a WebAssembly sandbox with no implicit access to anything — not the network, not the filesystem, not environment variables. If the manifest doesn't list http.allowedHosts: ["api.github.com"], neither your code nor a transitive npm dep can reach api.github.com. The host grants only what the manifest declares; everything else is denied. This is meaningful supply-chain protection: a compromised package upstream of your particle can't exfiltrate anything it doesn't already have explicit permission to touch.
  • Secure credential storage. Secrets are encrypted at rest with a key held in the OS keychain, and never made available to code running inside the particle. For OAuth, API keys, and Basic auth, the host fills in the real value as the request leaves the sandbox — your handler only ever sees an opaque placeholder. Signing keys work the same way: signing.sign(name, data) returns a signature without the key ever reaching JS. A malicious npm dep scanning memory or process.env gets nothing. Setup is a one-time CLI prompt.
  • No local toolchain. The CLI bundles npm resolution, TypeScript typechecking, and esbuild. No Node install, no node_modules, no build config — particle build reads Particlefile.ts, resolves every npm: import declared in source, and produces a single self-contained artifact.
  • Easy for an LLM to write. A particle is one file with one default export and a small set of fields (name, capabilities, tools). An agent that hits "I need a tool to do X" mid-task can write a Particlefile.ts, run particle build, and have that tool available through its own MCP or CLI surface for the rest of the session — and every session after. The sandbox means a hastily-written particle can't reach beyond what its manifest declared, even if the model misjudged what it was writing.
  • Easy to share. particle build --pack produces a self-contained .particle archive; the receiver installs it with particle import <file-or-url>. Before anything runs, they see exactly which capabilities the particle requests — hosts, credentials — and either accept or decline. The sandbox enforces those capabilities at runtime, so installing someone else's particle doesn't require trusting their code — just trusting that the manifest accurately describes the worst case.

A particle, top to bottom

Particles can be written in TypeScript / JavaScript or Python. The runtime contract and capability model are identical; the choice is author preference. The build pipeline picks the engine from the source filename — Particlefile.{ts,js} → JS runtime (QuickJS), Particlefile.py → Python runtime (CPython via componentize-py). Examples below are TypeScript; see examples/github-py/Particlefile.py for the equivalent in Python.

A particle lives in Particlefile.ts (or .js) at the root of a directory:

import { credentials } from "@partite-ai/particle-credentials";

export default {
  name: "github-tools",
  description: "A few GitHub API tools.",
  version: "0.1.0",

  capabilities: {
    http: { allowedHosts: ["api.github.com"] },
  },

  credentials: {
    github: {
      hosts: ["api.github.com"],
      required: true,
      methods: {
        pat: {
          type: "apikey",
          location: { kind: "auth-scheme", scheme: "Bearer" },
        },
      },
    },
  },

  tools: {
    get_repo: {
      description: "Fetch repository metadata.",
      inputSchema: {
        type: "object",
        properties: {
          owner: { type: "string" },
          repo:  { type: "string" },
        },
        required: ["owner", "repo"],
      },
      handler: async ({ owner, repo }: { owner: string; repo: string }) => {
        const fetcher = await credentials.fetcher("github");
        const res = await fetcher(`https://api.github.com/repos/${owner}/${repo}`);
        return res.json();
      },
    },
  },
};

The richer examples/github particle adds OAuth 2 as an alternate auth method for the same github credential, a few more tools, and a ping health check.

Quick start

cd examples/github

# Build the particle and register it in the local state DB.
# Walks credential setup the first time — pick OAuth or a PAT.
particle build

# Call the registered particle directly.
particle ping  github-tools
particle run   github-tools get_repo --owner=octocat --repo=hello-world

# Or expose every tool as an MCP stdio server — drop this into
# Claude Desktop, a Claude Code skill, Cursor, or any other
# MCP-aware client.
particle serve-mcp github-tools

The state DB lives at <user-config-dir>/particle/state.db (override with --db). Subsequent builds with the same (name, version) skip credential prompts.

A typical MCP-client config (Claude Desktop, Cursor, etc.) wires the particle in directly:

{
  "mcpServers": {
    "github-tools": {
      "command": "particle",
      "args": ["serve-mcp", "github-tools"]
    }
  }
}

Commands

Command What it does
particle build [--pack] Build the particle in CWD. Default: register it. --pack writes a <name>-<version>.particle archive instead.
particle import <file-or-url> Read an archive produced by --pack, run it through the same setup flow, and register it.
particle list / ls List every registered particle and its configured auth method.
particle delete <name>[@version] / rm Remove a registered particle. Without @version: every version.
particle reconfigure <name> Re-prompt for credentials. Lets you switch auth methods.
particle ping <name>[@version] Call the particle's ping health-check.
particle run <name>[@version] <tool> [tool-flags] Call a tool. --help after the tool name lists the schema-derived flags.
particle serve-mcp <name>[@version] Stdio MCP server. --only-tools=a,b and --exclude-tools=c,d filter what's exposed.

Version is optional anywhere it appears — omitted resolves to the highest registered semver.

Capabilities at a glance

A particle only sees what its capabilities declare. Importing a host capability module (@partite-ai/particle-credentials, @partite-ai/particle-oauth, @partite-ai/particle-signing) for a category not authorized by the manifest is a build error.

  • http: { allowedHosts: [...] } — outbound HTTP. Anything else fails with a "destination prohibited" error. Host matching is case-insensitive. Today this is the only capability; raw sockets and other categories are tracked under "What's next".

A per-particle key/value store is available via @partite-ai/particle-kv — it's a built-in, not a capability you declare. Two particles using the same key see independent values; one particle's storage is invisible to another.

Credentials

credentials is its own top-level field on the default export — it describes what secret material the particle needs, not a permission gate. Each entry names a logical credential (e.g., github, openai):

  • hosts: [...] — pins the credential to a set of HTTP destinations. The host substitutes the real value only on requests bound for one of those hosts; a stray placeholder in a request to anywhere else transmits literally, surfacing the bug rather than leaking the secret. Every host here must also appear in capabilities.http.allowedHosts. Omit hosts for credentials accessed entirely through the JS-side API (signing keys, raw values).
  • required: true | false — whether setup refuses to register the particle until a method is configured. Defaults to false.
  • methods: { <name>: { type, ... } } — one or more alternative authentication methods. Supported types: basic, oauth2, apikey, signing-key, raw. The user picks one at setup; only that one is provisioned, and switching later atomically replaces it.

Tools call credentials.fetcher("<credName>") to get a fetch-shaped function bound to that credential — the same call regardless of which method the user picked, as long as the methods land at the same wire location. When the manifest mixes methods with different shapes (e.g., a header-based PAT alongside a query-param key), credentials.getConfiguredMethod("<credName>") reports which method is active so the tool can branch.

TypeScript types

types/particle.d.ts defines the shape of the default export. To get type-checking on your particle:

import type { Particle } from "particle";

export default {
  // ...
} satisfies Particle;

A typed handler argument is recommended (the schema isn't projected into the type system; the runtime validates at the boundary):

handler: async ({ owner, repo }: { owner: string; repo: string }) => { /* ... */ }

Where to read next

  • docs/initial-design.md — full design spec for the runtime, build pipeline, capability model, and credential types.
  • examples/ — runnable particles (is-odd, github).
  • types/particle.d.ts — the Particle type plus every capability and credential variant.

How this compares

vs. Docker / VMs. Containers and VMs also give you isolation, but at OS weight: image layers, a daemon, seconds-to-start cold paths, and a permissions model that's typically wired up at docker run time rather than declared inside the artifact. Particles start in milliseconds and bake the capability list into the artifact itself, so the user sees what's being asked for before anything launches. Trade-off: a particle runs JavaScript today, not arbitrary binaries (see "What's next" — that's expanding).

vs. raw Node / Bun / Deno. A bare script trusts its dependencies with everything: filesystem, network, environment. A particle's transitive deps inherit the same denied-by-default surface as the particle itself — if a fashionable npm package decides to phone home, it can't, because api.evil.example isn't in allowedHosts. Deno's --allow-* flags cover similar ground at the process level; particles attach the policy to the artifact, so the policy travels with the code and the user (not the author) controls whether to grant it.

What's next

Particles are in active development. On the roadmap:

  • Filesystem access exposed as a mount.
  • Outbound sockets with a declared allow-list, mirroring the HTTP model.

Building the project

The particle binary embeds five WebAssembly components: three build-pipeline helpers (npm resolution, pip resolution, TypeScript type-check) and two runtimes (JS via QuickJS, Python via CPython). To rebuild the components you'll need:

  • Go 1.26+
  • Rust + the wasm32-wasip2 target
  • wasm-rquickjs (JS runtime + typecheck)
  • componentize-py (Python runtime)
  • Node + npm (the typecheck wasm bundles the TypeScript compiler)
  • Python 3.12 (componentize-py bakes it into the Python runtime image)

You may find the included devcontainer useful.

make            # builds every wasm component and copies them into the embed dirs
go build -o particle ./cmd/particle

Status

Early. APIs, on-disk formats, and the credential setup flow may change as the project matures.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors