Installed add-ons can register process-wide services through the versioned runtime object:
const runtime = globalThis.__piclaw_runtime;This API is available before add-on runtime entries load. Add-ons must feature-detect the required API and version rather than import Piclaw runtime source or private installed modules.
Declare runtime entries in the package manifest:
{
"pi": {
"runtime": {
"entries": ["runtime.ts"],
"load": "startup"
}
}
}load has two values:
"startup"loads after Piclaw has started its WebChannel and wired the runtime messaging handlers, before chat warmup and crash recovery resume work. Use it for chat transports and other process-wide services that must exist before the first agent session."lazy"loads when a status panel, config action or Adaptive Card intent first needs runtime contributions. It is the default whenloadis omitted and preserves existing add-on behaviour.
Entry paths must resolve to files inside the installed add-on package. Piclaw rejects lexical path traversal and symlinks that escape the package.
Runtime entry registration lasts for the Piclaw process. Installing or uninstalling an add-on requires the normal Piclaw restart; the process rebuilds the registry from currently installed packages.
Feature-detect the API before use:
const messaging = globalThis.__piclaw_runtime?.messaging;
if (messaging?.version !== 1) {
throw new Error("Piclaw messaging API v1 is required.");
}const unregister = messaging.registerChatTransport({
id: "remote-peer",
kind: "bang",
async send(request) {
// request.address is a validated one-hop bang address.
return {
status: "queued",
source_chat_jid: request.source_chat_jid,
target_address: request.address.raw,
};
},
});Installed add-ons may register only the bang transport. Core owns the local transport. Only one bang transport may be registered, and duplicate ownership fails explicitly. The unregister callback is idempotent.
const agents = await messaging.listAdvertisableAgents();The result contains non-archived web agent aliases and active status. It does not expose local chat JIDs.
const result = await messaging.resolveLocalTarget({
target_agent_name: "research"
});Provide exactly one of target_agent_name or target_chat_jid. Prefer agent aliases. Results are resolved, not_found, or ambiguous; v1 local aliases are unique, while ambiguous is retained for future resolvers.
const receipt = await messaging.deliverPeerMessage({
target_agent_name: "research",
content: "Please review this plan.",
mode: "queue",
source: {
peer_instance_id: "immutable-authenticated-id",
peer_fingerprint: "abc123-def456-ghi789",
peer_alias: "lab",
agent_name: "auditor",
message_id: "rmsg_123",
reply_address: "lab!@auditor"
}
});Call this only after the add-on has authenticated the peer. Piclaw validates bounded peer fields, resolves the local target, constructs the reserved peer_message content block, and delivers through the normal timeline/queue path. The add-on cannot supply content blocks or a source chat JID.
Message bodies are limited to 32 KiB. Unknown modes default to queue. Peer delivery metadata uses source: "addon.peer-message" so queued and persisted messages remain attributable.
Startup runtime entries can register signed/non-browser transport endpoints through:
const externalRoutes = globalThis.__piclaw_runtime?.externalRoutes;
if (externalRoutes?.version !== 1) {
throw new Error("Piclaw external routes API v1 is required.");
}
const unregister = externalRoutes.register({
addonId: "remote-peer",
prefix: "/api/addons/remote-peer/v1",
methods: ["GET", "POST"],
maxBodyBytes: 32 * 1024,
async handler(req, pathname, context) {
return new Response(JSON.stringify({ ok: true }));
}
});External routes are reserved for installed startup add-ons that authenticate their own transport requests. Piclaw dispatches /api/addons/<id>/... before browser session and CSRF guards, so these routes must not rely on browser authentication.
Core enforces:
- package ownership:
@scope/piclaw-addon-<id>orpiclaw-addon-<id>may claim only/api/addons/<id>; - registration only during the owning package's
load: "startup"import; - startup freeze, duplicate/overlapping prefix rejection, and reset on process restart;
GET/POSTmethod allowlists;- declared and streamed body caps, with a 1 MiB registration ceiling;
- a coarse 120 requests/minute source bucket per add-on;
- standard Piclaw request IDs, server timing and security headers;
- generic 500 responses when handlers throw.
The add-on remains responsible for protocol authentication, signatures, nonce/replay checks, trust state, endpoint-specific limits, payload validation, and response schemas.
Unknown paths within /api/addons/ return JSON 404 without redirecting to browser login. Generic extension routes registered through __piclaw_registerRoute remain browser-authenticated and CSRF-protected.
The unregister callback is idempotent. Add-on install/uninstall already requires a Piclaw restart; the registry is rebuilt from installed startup entries on the new process.
const dataDir = messaging.getAddonDataDir("remote-peer");The add-on ID must be a lowercase 1–64 character slug containing letters, digits, dots, underscores or hyphens. Piclaw creates <PICLAW_DATA>/addons/<id> and rejects path or symlink escapes. The add-on owns files under this directory, including its SQLite database and identity material.
The runtime API does not expose Piclaw's database handle. Relational add-on state belongs in an add-on-owned database; extension KV is suitable only for small preference values.