Pantry Store¶
The Pantry Store is the server-side API for the Jarvis Pantry — the package registry and submission pipeline for community-built commands. It manages submissions, dispatches test runs to GitHub Actions, verifies results via signed callbacks, and publishes approved packages.
Quick Reference¶
| Deployment | Fly.io (jarvis-pantry-store) |
| Source | jarvis-pantry/ |
| Framework | FastAPI + SQLAlchemy |
| Database | PostgreSQL (Fly managed) |
| Health endpoint | GET /health |
| Self-hosting | docker-compose.local.yaml (see Running Your Own Pantry) |
Submission Pipeline¶
- Submit — Developer POSTs a manifest + package URL via the Pantry web UI or CLI
- Resolve — Pantry resolves a pip dependency lockfile for the submission's declared packages
- Dispatch — Pantry triggers a
workflow_dispatchonjarvis-pantry-runnerwithsubmission_id,nonce, and the resolved lockfile as inputs - Run — The runner's two-job workflow tests the submission in an isolated Docker sandbox
- Callback — Runner POSTs a signed result to
/v1/submissions/{id}/container-result - Verify — Pantry verifies the HMAC signature and updates submission state to
publishedorrejected
Callback Security Model¶
Container test results are verified using HMAC-SHA256 over the raw request body, keyed by PANTRY_CALLBACK_SIGNING_KEY.
How It Works¶
The runner signs over {submission_id}|{nonce}|{request_body_bytes} and sends the digest in the X-Pantry-HMAC header. Pantry recomputes the digest from the raw request.body() bytes (not from a parsed JSON object) so the digest covers exactly the bytes that will be sent on the wire.
The nonce is a per-submission random value Pantry generates at dispatch time and stores in the database. It prevents replay attacks — a captured callback payload cannot be replayed against a different submission.
Startup Guard¶
When PANTRY_CONTAINER_RUNNER=github_actions, Pantry refuses to start if PANTRY_CALLBACK_SIGNING_KEY is unset. This prevents a misconfigured deploy from silently accepting unsigned callbacks.
Environment Variables¶
| Variable | Required | Description |
|---|---|---|
PANTRY_CALLBACK_SIGNING_KEY |
Yes (GHA runner mode) | Shared HMAC secret. Must match the PANTRY_CALLBACK_SIGNING_KEY GHA env secret in the pantry-callback environment on jarvis-pantry-runner. |
PANTRY_CONTAINER_RUNNER |
Yes | Set to github_actions in production. |
Key Rotation¶
See the Callback Signing Key Rotation runbook.
Database Notes¶
The submissions table uses a callback_nonce column (renamed from callback_token in jarvis-pantry#25 via alter_column — in-flight rows were preserved during the migration).
Lockfile Size Cap¶
The resolved dependency lockfile is shipped as a workflow_dispatch input. GitHub Actions has a ~64 KB ceiling on dispatch inputs, so Pantry enforces a 50 KB cap on resolved lockfiles and rejects submissions that exceed it.
Hashless lockfiles (pip-compile without --generate-hashes) are used since the hash-per-artifact format inflates lockfile size dramatically for packages with many wheel variants. Version pinning is preserved; only the wheel hashes are omitted.
Apt Package Allowlists¶
Submissions can declare apt_packages to request system package installation on the runner. Pantry validates these against allowlist YAML files that are bundled into the Docker image.
The config/ directory is copied into both Dockerfile and Dockerfile.fly via COPY config/ config/. It contains:
| File | Purpose |
|---|---|
config/apt-allowlist.yaml |
Packages permitted for installation |
config/apt-source-allowlist.yaml |
Apt sources permitted to be added |
config/post-install-allowlist.yaml |
Post-install operations permitted |
The allowlist check fails closed — if the config directory is absent from the image, any submission that declares apt_packages is rejected immediately:
If you are building or rebuilding the Docker image and see this error, verify that COPY config/ config/ appears in the Dockerfile before the application entrypoint.
Forge Package Generation¶
POST /v1/forge/generate generates a Jarvis package from a natural-language description. It's BYOK — the caller supplies their own LLM API key (llm_api_key in the request body), used once and never stored.
Because the endpoint is unauthenticated and triggers LLM generation, it carries a per-client-IP rate limit — the tightest cap in the app:
| Variable | Default | Description |
|---|---|---|
FORGE_GENERATE_RATE_LIMIT_PER_HOUR |
5 |
Per-IP cap on /v1/forge/generate requests per hour |
RATE_LIMIT_DISABLED |
false |
Set true in dev to bypass all rate limits (submission and forge-generation alike) |
Requests over the cap receive 429 Too Many Requests. The limit check runs before generation starts, so a rate-limited request never reaches the LLM.
Self-Hosting Features¶
Three opt-in features support a Pantry run on your own network. They are documented as a guide in Running Your Own Pantry; this section lists what the service exposes.
| Variable | Default | Description |
|---|---|---|
FORGE_LOCAL_LLM_ENABLED |
false |
Master switch for the local LLM features. Off, /v1/forge/local-llm* return 404 and no local model is listed. |
FORGE_LOCAL_BASE_URL / FORGE_LOCAL_API_KEY / FORGE_LOCAL_MODEL |
— | First-run defaults for the local server. Values saved through the API win. |
FORGE_LOCAL_TIMEOUT / FORGE_LOCAL_MAX_TOKENS |
900 / 8192 |
Read timeout in seconds and output cap for local models |
PANTRY_UPSTREAM_URL |
— | Public Pantry API to list next to local packages. Empty serves only what this Pantry holds. |
PANTRY_UPSTREAM_TTL_SECONDS / PANTRY_UPSTREAM_TIMEOUT_SECONDS |
300 / 8 |
Cache lifetime and request timeout for the mirror |
PANTRY_GITHUB_TOKEN |
— | Clone token for private repositories when adding a package. Never stored. |
ADMIN_API_KEY |
— | Required to add private packages and to change or probe the local LLM server; those endpoints refuse to run without it |
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/v1/forge/features |
none | {local_llm, private_packages, upstream_mirror, admin_key_configured} |
GET |
/v1/forge/local-llm |
none, or X-Admin-Key to see the address |
Saved settings: base_url, has_api_key, default_model, source. The key is never returned, and base_url is empty unless the request carries the admin key. |
PUT |
/v1/forge/local-llm |
X-Admin-Key (required) |
Save base_url, api_key (omit to keep, "" to clear) and default_model |
POST |
/v1/forge/local-llm/detect |
X-Admin-Key (required) |
List the models the server reports at /v1/models |
POST |
/v1/admin/commands/from-repo |
X-Admin-Key (required) |
Publish a package from a Git repository, with no AI review or container test |
DELETE |
/v1/admin/commands/{name}/local |
X-Admin-Key (required) |
Unpublish a package added from a repository |
Settings that change at runtime (the local server's address, key and default model) are stored in the app_settings
table. Everything else remains environment-only. With the mirror on, GET /v1/commands and /v1/categories merge local
and public packages and each item carries "source". The field is present with or without the mirror: "local" for a package an administrator
added from a repository, "pantry" for one published through this Pantry, "upstream" for a mirrored one. Local packages
belong to one reserved author row (is_local, stored username _local) that no login can reach.
Database driver
SQLAlchemy 2.1 maps a bare postgresql:// URL to the psycopg v3 driver, which this project does not install.
app/db.py and Alembic's env.py use postgresql+psycopg2:// instead.
Deployment¶
Alembic migrations run automatically on every container boot via the Fly CMD.