pantry docs

pantry is a live Cloudflare Worker and D1 shelf for exact saved code. Pi, OpenCode, your own orchestrator or agent, curl, and a plain Worker can fetch the same recipe; pantry never runs it.

What it is

A recipe is a named JavaScript function body with an input schema, capability tags, status, version, and owner provenance. pantry keeps recipes in D1 and hands the code back when asked. It never runs a recipe. The caller fetches a recipe, reviews the source, and chooses the execution authority.

The store is private by default. A bearer token maps to one owner, and an owner sees its own recipes first. Shared reads are opt-in artifacts for moving code between harnesses with author provenance visible before execution.

The recipe shape

A recipe is the JSON you push to POST /recipes:

{
  "name": "slugify",
  "description": "Turn arbitrary text into a URL-safe slug. Deterministic, no I/O.",
  "inputSchema": {
    "type": "object",
    "properties": { "text": { "type": "string", "maxLength": 500 } },
    "required": ["text"]
  },
  "code": "const text = String((ctx.input && ctx.input.text) || '');\nreturn { slug: text.toLowerCase().replace(/[^a-z0-9]+/g, '-') };",
  "capabilities": ["text.transform"],
  "status": "enabled",
  "sourceRunId": null
}

Field rules, enforced by validateRecipeInput in src/recipe.ts:

  • name must match /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/. One name per owner.
  • description is 5 to 500 characters.
  • inputSchema is an object whose type is "object". It defaults to { "type": "object", "properties": {} } when omitted.
  • code is required, is a JavaScript function body, and is at most 32000 bytes. The runner calls it with one argument, ctx.
  • capabilities must list at least one tag: a scoped namespace (workspace.*, machine.*, cloudbox.*) or a generic dotted tag such as text.transform. Tags are deduplicated and sorted.
  • status is "pending", "enabled", or "disabled". Unknown values become "enabled"; the Pi push path can save pending recipes for owner approval before enabling.
  • sourceRunId is an optional string, otherwise null.

The code field is a function body, not a module. It receives a single ctx object and returns a plain value.

The API

All routes except /health and OPTIONS require Authorization: Bearer <YOUR_TOKEN>. The examples below assume PANTRY_URL is set in your shell, and use a <YOUR_TOKEN> placeholder. The token is a secret; it is never shipped to a browser.

GET /health

Open, no auth. For uptime checks.

curl "$PANTRY_URL/health"
# {"ok":true,"service":"pantry"}

POST /recipes

Upsert a recipe. Creates on first push (201), bumps version and updates the row on a repeat push of the same (owner, name) (200). An invalid body returns 400 with { "error": ..., "code": "InvalidInput" }.

curl -X POST "$PANTRY_URL/recipes" \
  -H "authorization: Bearer <YOUR_TOKEN>" \
  -H "content-type: application/json" \
  -d '{
    "name": "slugify",
    "description": "Turn arbitrary text into a URL-safe slug.",
    "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] },
    "code": "return { slug: String(ctx.input.text).toLowerCase() };",
    "capabilities": ["text.transform"],
    "status": "enabled"
  }'
# {"name":"slugify","version":1}

GET /recipes

List recipes for the owner, ordered by updatedAt descending. The list never includes code. This is the discovery call and the review-before-run entry point.

curl "$PANTRY_URL/recipes" -H "authorization: Bearer <YOUR_TOKEN>"
# {"recipes":[{"name":"slugify","description":"...","inputSchema":{...},
#   "capabilities":["text.transform"],"status":"enabled","version":1,
#   "sourceRunId":null,"updatedAt":"..."}]}

GET /recipe/:name

The full recipe, including code. This is the call a caller makes right before running the recipe. Returns 404 when the recipe does not exist for this owner.

curl "$PANTRY_URL/recipe/slugify" -H "authorization: Bearer <YOUR_TOKEN>"
# {"name":"slugify",...,"code":"return { slug: ... };","createdAt":"..."}

DELETE /recipe/:name

Owner-scoped delete. Returns 404 when nothing was deleted.

curl -X DELETE "$PANTRY_URL/recipe/slugify" -H "authorization: Bearer <YOUR_TOKEN>"
# {"deleted":true,"name":"slugify"}

The Worker fails closed. With no PANTRY_TOKEN configured, every authenticated route returns 503. A wrong or missing bearer token returns 401. Only /health and the CORS preflight are open.

Use a recipe from your agent

A caller reaches pantry through the same registry verbs from several harnesses: a small client for code, the Pi tool for a session, OpenCode through its plugin, your own orchestrator through a sync bridge, or curl against the API.

src/client.ts reads PANTRY_URL and PANTRY_TOKEN from the environment by default. list() fails soft: an unconfigured client returns [] rather than throwing, so a recipe lookup degrades to ordinary reasoning instead of crashing the caller.

import { pantry } from 'pantry'; // ./src/client.ts

// Discovery: no code is transferred, so this is cheap.
const available = await pantry.list();

// Fetch the full recipe including code.
const recipe = await pantry.get('slugify');
if (recipe) {
  // recipe.code is a function body. The caller decides whether to run it,
  // and in what isolate. pantry does not run it for you.
}

The Pi tool wraps that same client so a Pi or terrarium session can reach pantry without curl. It registers one tool named pantry with four actions:

  • list returns names, descriptions, input schemas, and capabilities, without code.
  • get(name) returns the full recipe including code. The session reads this before deciding to run anything.
  • run(name, input) fetches the recipe and executes its code over an explicit ctx of { input }. This step runs fetched code.
  • push(recipe) upserts a recipe in the same shape the API accepts.

The tool's run action and the demo runner share one honest posture: running a fetched recipe is the caller's decision and the caller's risk. See Security.

Where recipes come from

A recipe reaches pantry one of two ways.

The direct way is a POST /recipes, by curl, the client, or the Pi tool's push. The repo's examples/recipes/ holds recipes authored this way.

The other way is a sync bridge from your own orchestrator. An orchestrator that keeps its own saved recipes maps each one to a pantry POST /recipes body. A well-behaved bridge is narrow: additive (nothing in the orchestrator's request path calls it; a caller opts in), env-gated (with no token it logs a no-op and returns), enabled-only (it pushes a recipe only when its status is enabled), and fail-soft (a network error, a rejected recipe, or a malformed row is logged and skipped). The token travels only in the Authorization header and is never logged.

Shared pantry

The same interface can read opt-in shared recipes by relaxing one boundary. Recipes are private unless their owner marks them visibility: "shared". There are no new core verbs: push can set visibility, list can ask for ?scope=shared, and get still returns a full recipe. Shared list rows show author, version, and status without code.

// Publish your own recipe to the shared shelf.
await pantry.push({ ...recipe, visibility: 'shared' });
// or: pantry push --shared recipe.json

// Discover shared recipes without transferring code.
const shared = await pantry.list({ scope: 'shared' });
// each shared row includes the author owner as provenance

// Fetch still uses the same name lookup. Your own recipe wins first.
const recipe = await pantry.get('slugify');

Writes stay owner-scoped: you can publish, unpublish, update, or delete only your own row. Shared recipes expose the author owner so a caller can reason about provenance before fetching code. pantry still never runs the recipe; the caller runs fetched code in its own isolate if it chooses to run it at all.

Security

pantry stores and hands back a script. It never executes a recipe. The bytes in code are returned verbatim from D1.

Running a fetched recipe is the caller's decision and the caller's risk. The example runner is a demo, and its header says plainly that it is not a trust boundary. It binds a few ambient names such as fetch and process to undefined as a convenience, and it runs a best-effort parse-time scan for obvious escape tokens. Both are tripwires. The file documents the escapes that defeat the shadowing, including import('node:fs') and the Function-constructor climb back to global scope. A passing scan proves nothing about safety.

If you do not already trust a recipe's author, run the recipe in a real isolate: a Cloudflare Worker Loader, a separate Worker, a child process, or a vetted JS sandbox. Real isolation is the caller's job, and pantry does not do it.

Server-side defenses pantry does provide:

  • Bearer-token gate, fail-closed. No token configured returns 503. Wrong or missing token returns 401. The compare is constant-time so it does not leak token length or prefix through timing.
  • Owner scoping. A token maps to one owner. Every query filters by owner, so one owner cannot read or delete another owner's recipes.
  • CORS echoes the request origin and never pairs the wildcard with credentials. Preflight is answered before the auth gate.
  • The token is a wrangler/alchemy secret. It is never hardcoded and never shipped to a client.

Eval note

The main pantry claim is custody of exact reviewed artifacts across harnesses. The older eval still records a bounded token observation: fetching saved code can reduce generated output for repeated procedures, while discovery and tool-invocation overhead can make total tokens rise for small procedures.

There is still a per-call discovery cost. The model has to know a recipe exists, read its description and input schema, and decide it fits. GET /recipes keeps that cost low by omitting code. Novel work still needs reasoning, because there is no saved artifact to fetch.

The proof page shows the current local eval. It labels prompt-side token counts as estimates when live mode has no provider usage, and it labels pantry execution time and correctness as measured local deterministic work. One exploratory prod Kimi K2.7 sample saw reuse reduce output tokens and raise total tokens for a tiny procedure because of input/tool overhead. A clean multi-sample benchmark remains open. Read the proof.

Limits

  • code is capped at 32000 bytes.
  • description is 5 to 500 characters.
  • One recipe name per owner. A repeat push upserts and bumps version; it does not create a second row.
  • inputSchema.type must be "object".
  • pantry stores recipes; it does not validate that code is correct, safe, or matches its inputSchema. Validation covers shape and size, not behavior.
  • pantry never runs a recipe, so it enforces nothing about what code does at runtime. capabilities are tags for the caller to reason about, not a sandbox.
  • D1 limits apply to row size and database size.