Skip to content

Overlay Packages & the Extension SDK

A pi package is the standard unit of distribution for pi extensions, skills, prompts, and themes. An overlay package is a normal pi package that also declares PizzaPi-specific capabilities — runner services, agents, rules, and MCP servers — under a pi.pizzapi key in its package.json.

The point of the overlay is that it is additive. One package installs cleanly in vanilla pi and in PizzaPi; vanilla pi simply ignores the pi.pizzapi block, and PizzaPi mounts the extra capabilities on top.

Overlay packages and runner services are not two features. The overlay is the packaging and trust layer — how capabilities are declared, installed, scoped, and granted. A runner service is one kind of capability the overlay can carry: the daemon-side process that ships handlers, panels, triggers, and sigils. This page covers the manifest, trust, and SDK; the runner services page covers what a service does once it is loaded.


Everything PizzaPi-specific lives under pi.pizzapi in package.json:

package.json
{
"name": "@example/my-tools",
"version": "1.0.0",
"type": "module",
"pi": {
"skills": ["./skills"],
"pizzapi": {
"schemaVersion": 1,
"agents": ["./agents"],
"rules": ["./rules"],
"mcp": "./.mcp.json",
"services": [
{
"id": "my-service",
"label": "My Service",
"icon": "activity",
"entry": "./service/index.ts",
"panel": { "dir": "./service/panel" },
"triggers": "./service/triggers.json",
"sigils": "./service/sigils.json"
}
]
}
}
}
  • package.json
  • Directoryskills/
    • …
  • Directoryagents/
    • …
  • Directoryrules/
    • …
  • .mcp.json
  • Directoryservice/
    • index.ts
    • triggers.json
    • sigils.json
    • Directorypanel/
      • index.html

Field Type Purpose
schemaVersion 1 Required. Pins the overlay schema.
services PizzaPiServiceDeclaration[] Runner services. Require a separate trust grant — see Trust.
agents string[] Directories of agent definitions.
rules string[] Directories of rules injected into session context.
mcp string Path to an MCP server definition file.

Each entry in services needs id, label, and entry; icon, panel, triggers, and sigils are optional. The full field reference, handler contract, and lifecycle live on the Runner Services page — this page only covers how the declaration is packaged and trusted.

Values in an overlay MCP definition expand these tokens:

Token Expands to
@PACKAGE_ROOT@ The installed package’s root directory
@HOME@ The user’s home directory
@PWD@ Current working directory
@PROJECT_DIR@ Project directory
@SESSION_ID@ Active session ID
@USER@ Current username

@PACKAGE_ROOT@ is what makes a package relocatable — use it for binaries and scripts shipped inside the package:

.mcp.json
{
"mcp": {
"servers": [
{
"name": "my-server",
"transport": "stdio",
"command": "@PACKAGE_ROOT@/bin/my-server",
"args": ["serve"],
"env": { "MY_CONFIG": "@HOME@/.config/my-tool.json" }
}
]
}
}

@pizzapi/extension-sdk is the public authoring contract — the TypeScript types and the host-detection helpers that overlay packages are written against.

import type {
// Overlay manifest
PizzaPiOverlayV1, PizzaPiServiceDeclaration, PanelVariable,
// Runner services
ServiceHandler, ServiceInitOptions, ServiceEnvelope, PizzaPiSocket,
ReconcileResult, ReconcileOptions,
TriggerSubscriptionEntry, TriggerSubscriptionDelta,
// Host detection
PizzaPiHostInfo, PizzaPiHostAPI,
} from "@pizzapi/extension-sdk";
import {
isPizzaPiHostInfo, detectPizzaPiHost, onPizzaPiHost,
} from "@pizzapi/extension-sdk";

Types are erased at build time; only the three host-detection helpers are runtime code. The ServiceHandler contract itself — init/dispose, ServiceInitOptions, and the optional lifecycle hooks — is documented under ServiceHandler API.


The same package may be loaded by vanilla pi, where no runner daemon, relay, or web UI exists. Use host detection so PizzaPi-only code paths simply don’t activate there.

detectPizzaPiHost() is a synchronous probe — it returns host info only if a PizzaPi host answers on the same tick:

import { detectPizzaPiHost } from "@pizzapi/extension-sdk";
export default function myExtension(pi) {
const host = detectPizzaPiHost(pi);
if (!host) return; // vanilla pi — degrade quietly
// PizzaPi-only setup here
}

Because packages can load before the host announces itself, prefer onPizzaPiHost(), which fires immediately if the host is already up and otherwise waits for its ready event. It delivers at most once and returns an unsubscribe function:

import { onPizzaPiHost } from "@pizzapi/extension-sdk";
export default function myExtension(pi) {
const unsubscribe = onPizzaPiHost(pi, (host) => {
if (!host.capabilities.includes("services")) return;
// safe to use PizzaPi capabilities
});
return { dispose: unsubscribe };
}

PizzaPiHostInfo carries apiVersion: 1 and a capabilities string array. Check capabilities before relying on a feature rather than assuming a version implies it. isPizzaPiHostInfo() is exported for validating a payload you received yourself.


Installing a package and letting it run daemon services are two separate decisions. Code in a runner service runs on the daemon, outside any session sandbox, so it always requires an explicit grant:

Terminal window
# Install and grant declared runner services in one step
pizza install ./my-package --allow-daemon-services
# Install now, decide later
pizza install ./my-package --no-allow-daemon-services
pizza config grant ./my-package
pizza config grant ./my-package my-service # grant one service
pizza config revoke ./my-package my-service

Grants are recorded in overlayServiceGrants in ~/.pizzapi/config.json.

Scope Agents / rules / MCP Runner services
User (pizza install ...) Yes Yes, with a grant
Project (pizza install ... -l) Yes, after pi project trust No — not in schema version 1

Project packages that declare services still install; PizzaPi warns that those services stay inactive. The daemon owns a single service registry shared by every workspace, so mounting a service found in one checkout would activate that project’s code for unrelated sessions. See Runner Services for the full scope rule.

Built-in service IDs (terminal, file-explorer, git, memory, process, time, tunnel) are reserved and always win. Between packages, the first to claim an ID in a discovery pass keeps it and later claimants are skipped with a warning.


Terminal window
pizza list
PizzaPi overlay:
● local:/path/to/my-package (user) — agents:1 rules:1 mcp services:[my-service:granted]

Confirm the daemon actually mounted it:

Terminal window
grep "loaded package service" ~/.pizzapi/logs/runner.log
[services] loaded package service "my-service" from local:/path/to/my-package