Loading repository data…
Loading repository data…
acoyfellow / repository
Agents fetch exact saved code from pantry instead of re-deriving it; pantry never runs the code. A capability-scoped recipe store on Cloudflare Workers + D1.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
pantry is a durable, harness-agnostic shelf for recipes. A recipe is a named function with an input schema, capability tags, status, version, and owner provenance. pantry stores the source in D1 and hands it back on request. It never runs a recipe; the caller reviews the exact saved artifact and chooses its own execution authority.
A recipe is a named JavaScript function 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 the exact saved source, reviews it, and chooses the execution authority.
The store is private by default. A bearer token maps to one owner, and the default list shows only that owner's recipes. Many owners can share one deployment: set PANTRY_TOKENS to a JSON map of { "<token>": "<owner>" } and each token authenticates as its own owner (single-tenant PANTRY_TOKEN + PANTRY_OWNER remain the fallback). A private recipe never crosses an owner boundary. Shared pantry is opt-in: an author can mark their own recipe "visibility":"shared" so other owners may read it with author provenance, version, and status. Writes stay owner-scoped, and pantry still never runs a recipe.
Think extensions are runtime-local capabilities for a Think agent; pantry is a runtime-neutral code registry where any harness fetches the exact saved source and chooses its own execution authority. Runtime extensions live inside one agent. A pantry recipe is a portable artifact any harness can fetch.
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 and must be at most 32000 bytes. pantry accepts three runner shapes: a bare JavaScript function body, an export default function/expression, or a module.exports function/expression. The demo runner calls bare bodies with one argument, ctx; exported callables receive (input, ctx).capabilities must list at least one tag. A tag is either 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.visibility is "private" by default. Set "shared" to opt your own recipe into the shared read pool.The code field returns a plain value. The demo runner accepts the shapes an agent naturally authors: a bare function body that reads ctx, an export default (input, ctx) => ... (or export default function run(input, ctx)), or a module.exports = (input, ctx) => .... The sample in examples/sample-recipe.ts uses the bare-body shape, reads ctx.input.text, and returns { slug }.
Run it locally with wrangler. Install, apply the D1 migration to the local database, set a token in .dev.vars (gitignored), then start the dev server:
bun install
bunx wrangler d1 migrations apply pantry-db --local # creates/updates the local D1 schema
echo 'PANTRY_TOKEN=dev-secret' > .dev.vars
bun run dev
The migration step is required. The D1 binding is DB and the database is pantry-db; the schema lives in migrations/0001_recipes.sql. Without it the local database has no recipes table, and every route that touches D1 returns a 500 with no such table: recipes. Run the migration once per fresh local database. For a deployed instance, apply the same migration with --remote instead of --local.
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.
The public instance lives at https://pantry.coey.dev (gated; the only open route is /health). All routes except /health and OPTIONS require Authorization: Bearer <PANTRY_TOKEN>. Examples below assume PANTRY_URL and PANTRY_TOKEN are set in your shell.
GET /healthOpen, no auth. For uptime checks.
curl "$PANTRY_URL/health"
# {"ok":true,"service":"pantry"}
Single-player remains the default. Shared pantry widens reads only, with no new core verbs:
pantry push recipe.json # private
pantry push recipe.json --shared # publish your own recipe
pantry list # your recipes
pantry list --shared # shared recipes, no code, includes author
pantry get Name # own recipe wins, then shared
API shape: GET /recipes?scope=shared lists shared recipes across owners without code and includes author, version, and status. GET /recipe/:name resolves your own recipe first, then the most recently updated shared recipe with that name. Private recipes never cross owner boundaries. Provenance is informational: a shared recipe is still fetched code, and the caller decides whether and where to run it.
POST /recipesUpsert a recipe. Creates on first push (201), bumps version and updates the row on a repeat push of the same (owner, name) (200). Optional visibility is private or shared; only your own row is written.
curl -X POST "$PANTRY_URL/recipes" \
-H "authorization: Bearer $PANTRY_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": "const text = String((ctx.input && ctx.input.text) || \"\");\nreturn { slug: text.toLowerCase().replace(/[^a-z0-9]+/g, \"-\") };",
"capabilities": ["text.transform"],
"status": "enabled"
}'
# {"name":"slugify","version":1}
An invalid body returns 400 with { "error": ..., "code": "InvalidInput" }.
A valid push runs a best-effort lint and returns any warnings alongside name and version. It flags code that will not run deterministically (Math.random, Date.now, new Date, fetch, and similar) and a bare function body that reads input instead of ctx.input. The lint is a coarse token scan, not a proof: a clean lint does not guarantee a recipe is deterministic, and a recipe can defeat the scan. It does not reject by default; add ?strict=1 to reject a recipe that fails the lint with a 422.
GET /recipesList 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. Add ?scope=shared to list shared recipes from all owners with author provenance, still without code.
Filter discovery so its cost stays bounded by relevance as the cookbook grows: ?q= matches a keyword over name and description, and ?capability= matches a capability tag. Filters compose with ?scope=, and the list still never includes code.
curl "$PANTRY_URL/recipes" -H "authorization: Bearer $PANTRY_TOKEN"
# {"recipes":[{"name":"slugify","description":"...","inputSchema":{...},
# "capabilities":["text.transform"],"status":"enabled","version":1,
# "sourceRunId":null,"updatedAt":"..."}]}
GET /recipe/:nameThe full recipe, including code. This is the call a caller makes right before reviewing and possibly running the recipe. Your own recipe wins; if missing, pantry returns the most recently updated shared recipe with that name. Returns 404 when neither exists.
curl "$PANTRY_URL/recipe/slugify" -H "authorization: Bearer $PANTRY_TOKEN"
# {"name":"slugify",...,"code":"const text = ...","createdAt":"..."}
DELETE /recipe/:nameOwner-scoped delete. Returns 404 when nothing was deleted.
curl -X DELETE "$PANTRY_URL/recipe/slugify" -H "authorization: Bearer $PANTRY_TOKEN"
# {"deleted":true,"name":"slugify"}
A caller reaches pantry through the same registry verbs from several harnesses: an MCP server for any MCP client, 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.
The MCP server (pantry mcp) exposes pantry_list, pantry_get, pantry_run, and pantry_push over stdio, so Claude Desktop, Cursor, or another MCP client reaches the same recipes. See docs/MCP.md for the client config and the trust posture.
src/client.ts is a small client usable from terrarium, Pi, or a Worker. It 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.
}
// Save a recipe back.
await pantry.push({
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',
sourceRunId: null,
});
The Pi tool wraps that same client so a Pi or terrarium session can reach pantry without curl. It lives at .pi/extensions/pantry/index.ts, registers one tool named pantry, and exposes four actions:
list returns names, descriptions, input schemas, and capabilities, without code. This is the discovery call and the review-before-run entry point.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; see below.push(recipe) upserts a recipe in the same shape the API accepts.