This document defines the extension architecture for pi_agent_rust with the goal of maximum compatibility, formal safety guarantees, and measurable performance. The system is best‑effort by default, but designed to converge to full parity with legacy Pi extensions.
- Compatibility: run legacy Pi extensions with best‑effort fidelity.
- Performance: <2ms p95 overhead per tool call (excluding tool work).
- Safety: explicit, auditable capability grants with optional strict mode.
- Stability: versioned protocol + conformance fixtures.
- Portability: same artifact runs on Linux/macOS/Windows.
Non‑goals:
- Custom TUI rendering from extensions (core owns the UI).
- Node‑native addons (must use hostcalls or WASM).
Tier A — WASM Component (default):
- Fast, sandboxed, portable.
- Typed hostcalls via WIT.
Tier B — JS Compatibility (compiled):
- Legacy TS/JS compiled to a single bundle.
- Pre‑compiled to QuickJS bytecode or JS→WASM.
- No Node runtime required.
Tier C — MCP (process IPC):
- For heavy integrations: IDEs, databases, cloud services.
WASM is the default. JS compatibility is a compile step, not a runtime.
Mario’s critique is correct in the narrow sense: QuickJS is just a JS engine. It intentionally does not ship a Node/Bun‑style OS API surface or a full general-purpose event loop.
Our answer is: great — we don’t want the Node/Bun surface area.
Instead, Pi provides a tiny, capability‑gated connector layer and an explicit event loop that is tailored to Pi’s extension needs (not the entire web/Node ecosystem).
Extensions do not get raw OS access (no fs, no child_process, no arbitrary
sockets). They get a small set of hostcalls that map to Pi’s already-audited
operations (tools + session/ui actions).
Core examples (names illustrative):
pi.tool(name, input)→ delegates to the built-in tool registry (read/write/edit/bash/grep/find/ls/hashline_edit)pi.exec(command, args, options)→ a constrained process runner (timeout + process-tree cleanup)pi.fs.*→ a capability filesystem rooted at project/cwd (no path escape)pi.http(request)→ a constrained HTTP client (policy-controlled)pi.session.*,pi.ui.*,pi.events.*→ Pi-internal APIs (no OS exposure)
This is strictly smaller than Node/Bun, and it is auditable: every connector call is an explicit, logged capability check.
QuickJS supports Promises/microtasks; it just needs a host to drive them.
We provide a tiny “Pi event loop”:
- Drain the QuickJS job queue (microtasks)
- Poll outstanding host operations (Rust futures via tokio/asupersync)
- Resolve/reject the corresponding JS Promises
- Repeat until idle (or until a deadline/timer fires)
In other words: Node’s event loop is a product; ours is a proof obligation: it only implements what Pi needs, with deterministic testing hooks.
Security: Node/Bun expose an enormous ambient-authority surface by default. Our connector layer is capability-based and narrow by construction.
Performance: Node/Bun pay startup/memory costs for massive compatibility. We precompile JS to bytecode (or WASM) and the runtime only contains:
- a JS engine + 2) a small dispatcher + 3) our connectors.
Determinism: With asupersync (LabRuntime) we can test extension async + time deterministically (no “real time” flakiness).
This section defines the authoritative PiJS runtime contract for running JS/TS extensions without Node/Bun, with a deterministic, testable event loop and an explicit, capability-gated hostcall surface.
This contract is the reference for the JS runtime, scheduler, hostcall bridge, and test harness workstreams.
- Assume QuickJS has no WebAssembly: any JS bundle expecting
globalThis.WebAssemblymust use the PiWasm bridge (or Tier A WASM components). - No ambient OS APIs: all side effects must flow through the connector dispatcher (capability checks + structured audit logs).
- PiWasm import linking is fail-closed: modules only receive the host imports
implemented in
src/pi_wasm.rs. Unsupported function/table/global/memory imports fail during instantiation. Default-return compatibility stubs are limited to the explicit Emscripten names inCOMPAT_STUB_IMPORTSand do not grant host filesystem, network, or process authority.
- Microtasks: the QuickJS job queue (Promise reactions,
queueMicrotask). - Macrotasks: host-driven tasks (timers, inbound extension events, hostcall completions).
- Tick: one deterministic scheduling step that runs at most one macrotask plus a full microtask drain.
- Hostcall: a side-effecting request from JS to the host, represented in
protocol terms as
host_call/host_result(see §3.2).
PiJS executes extension artifacts produced by extc (the compiler pipeline) from
pinned sources (see docs/extension-sample.json).
The compiled output MUST be:
- deterministic (byte-for-byte stable under identical inputs)
- ESM-resolvable inside PiJS
- sourcemap-correct (runtime errors map to original TS/JS)
The PiJS module resolver MUST:
- canonicalize Node builtins to
node:*(fs→node:fs,path→node:path, etc.) - resolve virtual builtins first (
node:*shims + Pi runtime virtual modules) - resolve file-backed modules with this deterministic order:
- exact file path
- directory index (
index.ts,index.tsx,index.js,index.mjs,index.json) - extension fallback (
.ts,.tsx,.js,.mjs,.jsonas applicable)
- accept relative (
./,../), absolute (/...), andfile://...specifiers - reject bare package specifiers (no
node_modulestraversal in PiJS) - reject network imports (
http:/https:) and other ambient loaders
Deterministic unsupported-case errors:
- package imports:
Package module specifiers are not supported in PiJS: <specifier> - network imports:
Network module imports are not supported in PiJS: <specifier> - other unsupported forms:
Unsupported module specifier: <specifier>
node:module.createRequire() currently supports only Node builtins exposed by
PiJS and intentionally rejects package/local filesystem resolution.
- The host loads the artifact entry module.
- The entry module MUST export a default function that receives a host-
provided
piobject (the Extension API surface). - Any thrown error during load/initialization MUST be mapped to an extension error with sourcemapped location and emitted as structured log events.
The pi object provided to extensions is the single ambient authority. It MUST
be capability-gated internally.
At minimum (shape may follow the legacy API):
pi.registerTool(spec)pi.registerSlashCommand(spec)pi.on(event_name, handler)for lifecycle/tool-call hooks
Semantics:
- Registration MUST be idempotent per
(extension_id, name). - Invalid specs MUST fail fast with actionable errors.
- Registration controls what the host advertises/dispatches for that extension.
At minimum:
pi.tool(name, input) -> Promise<ToolOutput>pi.exec(cmd, args, options) -> Promise<{ stdout, stderr, exitCode }>pi.http(request) -> Promise<response>pi.session.*accessors/mutations as defined by the protocolpi.ui.*primitives (select/input/confirm/editor) that can be denied in non-interactive modepi.log(level, event, data)for extension-authored logs
Rules:
- Every connector method maps to a
host_callwith acall_id, capability, method, params, and timeout/cancel metadata (§3.2). - Every connector method MUST emit structured audit logs (see §3.1 / §3.4).
- Errors MUST map onto the hostcall error taxonomy (§3.2): Denied/Timeout/IO/ InvalidRequest/Internal.
- Any async connector call MAY accept an
AbortSignal; cancellation MUST map to hostcall cancel-token semantics. - Timeouts MUST be enforced in the dispatcher; JS receives a deterministic Timeout error.
Define the runtime state as:
seq: u64monotone counter (total-order tie-breaker)Q_micro: the QuickJS job queue (internal to engine; host can drain)Q_macro: FIFO queue of macrotasks, each tagged with an enqueueseqQ_timer: min-heap of timers keyed by(deadline_ms, seq)clock: a monotonic time source (injectable for tests)
Each macrotask is one of:
TimerFired(timer_id)HostcallComplete(call_id, outcome)InboundEvent(event_id, payload)(tool_call, slash_command, lifecycle hook, UI response, etc.)
tick(state) MUST be deterministic given the current state and the set of newly
arrived host completions.
Algorithm:
- Ingest host completions: any completed hostcalls since the last tick are
enqueued into
Q_macrowith a deterministic order key.- Recommended: assign each completion an enqueue
seqin arrival order using the monotone counter.
- Recommended: assign each completion an enqueue
- Move due timers: while
Q_timer.min.deadline_ms <= clock.now_ms, pop timers and enqueueTimerFiredintoQ_macro(preserving(deadline_ms, seq)order). - Run one macrotask:
- If
Q_macrois non-empty: pop the lowestseqmacrotask and execute it. - Else: idle (no-op).
- If
- Drain microtasks to fixpoint: repeatedly drain the QuickJS job queue until it is empty.
- Return updated state.
- I1 (single macrotask): at most one macrotask executes per tick.
- I2 (microtask fixpoint): after any macrotask, microtasks are drained until empty.
- I3 (stable timers): timers with equal deadlines fire in increasing
seqorder. - I4 (no reentrancy): hostcall completions do not synchronously re-enter JS; they enqueue macrotasks.
- I5 (total order): all externally observable scheduling is ordered by
seq(deterministic tie-break).
setTimeout(fn, ms)enqueues a timer with(deadline_ms = clock.now_ms + ms, seq = next_seq()).clearTimeout(id)removes it if pending.setIntervalis optional unless required by the pinned sample; if implemented, it MUST be specified in terms of repeatedsetTimeoutwith stable ordering.
- Each hostcall has a stable
call_idand (recommended) an issuanceseq. - Completion enqueuing MUST be deterministic:
- In production: order by completion arrival, then stabilize with the monotone
seq. - In tests: completion order can be controlled by recorded fixtures / deterministic runtime.
- In production: order by completion arrival, then stabilize with the monotone
Given:
- identical artifact bytes + shim versions
- identical initial state
- identical sequence of inbound events (tool calls, lifecycle events, UI responses)
- identical sequence of hostcall results (including their enqueue order)
- identical clock behavior (or a deterministic clock)
Then:
- the sequence of executed macrotasks and the resulting observable outputs (tool results, logs, UI prompts) are identical.
- The scheduler is a pure function of
(state, arrivals)with a total-order tie-breakerseq. - Timer ordering is deterministic via
(deadline_ms, seq). - Hostcall completion ordering is deterministic by construction (completion
enqueue
seq). - Microtask draining to a fixpoint ensures no hidden interleavings.
- Therefore, by induction over ticks, the entire execution trace is deterministic under fixed inputs.
- Every tick and every enqueue/dequeue event MAY be logged (debug-level) under
pi.ext.log.v1withtrace_id/span_idand correlation ids. - Deterministic test runs MUST be able to compare traces for equality after the normalization rules in §3.1.
This section defines the canonical extension shapes we support and maps each shape to its entrypoint/config and required host capabilities. It is the reference for selection, conformance, and documentation work.
Runtime extensions (executable):
- PiJS (JS/TS) — legacy extensions compiled to JS (Tier B).
- WASM component — WIT-based components (Tier A).
External servers (out-of-process):
- MCP server — stdio/http/sse tool server (Tier C).
Resource packs (non-executable):
- Skill pack —
SKILL.mdbundles for agent behavior. - Prompt template —
.mdprompt files. - Theme —
.jsontheme definitions for UI.
Bundles/packages (distribution):
- Package source — a bundle that may include any of the above (extensions,
skills, prompts, themes). Resolved by
src/package_manager.rs.
| Shape | Entrypoint / Config | Runtime | Primary I/O Surface | Notes |
|---|---|---|---|---|
| PiJS extension | extension.json (pi.ext.manifest.v1) or package manifest listing extensions; entry .ts/.js |
QuickJS + Pi event loop | register + host_call/host_result |
Legacy TS/JS compiled and shimmed; no Node/Bun. |
| WASM component | extension.json with runtime="wasm"; entry .wasm component |
Wasmtime (component model) | WIT hostcalls → host_call/host_result |
Typed hostcalls via WIT. |
| MCP server | MCP config (*.json) or CLI args |
External process / remote server | MCP protocol (stdio/http/sse) | Not the extension protocol; policy-gated by connector. |
| Skill pack | SKILL.md + optional assets |
None (resource) | File load only | Injected into prompt context; no hostcalls. |
| Prompt template | .md prompt files |
None (resource) | File load only | Used by /template invocation. |
| Theme | .json theme file |
None (resource) | File load only | Used by TUI renderer. |
| Package source | package.json / package manifest with resources |
Mixed | Depends on contained resources | May include extensions + skills + prompts + themes. |
Capabilities are always derived from hostcalls (never trusted from the extension), but registration types imply typical capability usage:
| Registration type | Protocol surface | Typical hostcalls | Derived capabilities | Notes |
|---|---|---|---|---|
Tool (registerTool) |
register → tool_call/tool_result |
pi.tool(...) / pi.exec(...) |
read / write / exec / tool |
read/write/exec derived by tool name; unknown tools map to tool. |
Slash command (registerCommand) |
register → slash_command/slash_result |
pi.ui.*, pi.session.*, optional pi.exec |
ui / session / exec |
Commands are UI-driven; exec is optional. |
Event hook (event_hook) |
register → event_hook |
pi.session.*, pi.ui.*, pi.exec, pi.http |
session / ui / exec / http |
Capabilities depend on event handler behavior. |
Provider (registerProvider) |
register + streaming hooks |
pi.http(...) |
http (+ read if local files) |
Providers require network; record API key access as env if used. |
Flag (registerFlag) |
register only |
none until used | none (at register) | Flags are config; capabilities are driven by later behavior. |
Shortcut (registerShortcut) |
register only |
pi.ui.* on activation |
ui |
Shortcuts are UI-level triggers. |
Non-executable resource packs (skills/prompts/themes) do not invoke hostcalls and therefore have no runtime capability requirements beyond file loading.
This section is the research foundation for extension compatibility work. It documents where we discover candidates, how we validate them, and the canonical metadata we track so downstream beads can rank/select without re‑doing discovery.
We classify candidates by source tier (not by runtime tier):
official-pi-mono— the canonical upstream corpus (the “official 60” plus any additional pinned upstream examples).community— small community repos/gists; often single‑file extensions.third-party-github— larger third‑party repos (may be multi‑file).npm-registry— published packages that contain Pi extensions.agents-mikeastock— special-case curated corpus (kept as its own tier so we can reason about provenance).non-conformance— interesting but explicitly out-of-scope for parity (kept for research/triage only).
The tier labels above match the static-scan and master-catalog artifacts in
docs/ (see §1C.4).
Authoritative discovery sources (v1, ordered):
- Upstream pi-mono (
badlogic/pi-mono) extension examples corpus (canonical “official” reference set). - Curated corpora snapshots checked into this repo (e.g. under
legacy_pi_mono_code/), used for deterministic scanning and reproducible conformance runs. - GitHub discovery sweep (keyword + topic search) → candidate repos and raw files (tracked by research beads).
- npm registry sweep (keyword search + reverse-dep trails) → candidate packages and tarballs (tracked by research beads).
- Marketplaces/registries (when applicable) such as OpenClaw/ClawHub inventories (tracked by research beads).
We treat discovery as a pipeline, not a one-off search:
- Enumerate corpus roots per tier (local repo snapshots, git checkouts, npm package tarballs).
- Static scan:
- Find candidate entrypoints (default export /
register(...)patterns). - Record “capability signals” (imports/calls that imply hostcalls).
- Emit a machine-readable inventory for dedup + triage.
- Find candidate entrypoints (default export /
- Dynamic validation (ground truth):
- Load each candidate in the pi-mono TS runtime (Bun-based harness).
- Record load success/failure, error class, and registration output.
- Note: action methods may intentionally throw during load; we only require registration to succeed.
- Consolidate + deduplicate into a master candidate pool.
- Enrich + rank (only after the pool is stable):
- Size, file-count, dependency shape, IO patterns, popularity signals.
- Produce tiered execution plans (conformance ordering, complexity buckets).
The same logical extension can show up via multiple paths (forks, mirrors, vendored copies, npm repacks). We deduplicate using canonical source keys and content checksums:
- Canonical source key (stable identity):
- Git:
git:<repo_url>#<path> - npm:
npm:<package_name>@<version>#<path>(or omit@<version>if unknown) - local snapshot:
local:<absolute_path>
- Git:
- Content checksum (stable content):
sha256(file_bytes)(single-file) orsha256(concat(sorted(file_checksums)))(multi-file directory).
Rules:
- Prefer upstream canonical URLs when known (avoid per-fork “new identities”).
- When two candidates share a checksum, treat them as duplicates unless the runtime behavior differs under dynamic validation.
- Human-readable
idshould be stable when possible (manifest id or filename), but the source key + checksum are the real identity.
We keep the research outputs in docs/ so they can be reviewed, diffed, and
used by CI/harnesses:
docs/extension-entry-scan.json— static scan inventory (entrypoints + submodules + confidence + per-tier stats).docs/extension-master-catalog.json— deduplicated master pool for conformance (all tiers, minimal fields + checksums).docs/extension-catalog.json— enriched metadata for the full validated corpus (223 extensions across all source tiers, with conformance status, capabilities, IO patterns, complexity buckets, checksums, and perf budgets).docs/extension-catalog.schema.json— JSON Schema fordocs/extension-catalog.json(pi.ext.catalog.v1).docs/extension-priority.json— ranking/order plan for the official corpus (testability-first execution strategy).
Downstream beads should treat these as inputs and avoid re-scraping/re-scanning unless they are explicitly rebuilding the pipeline.
docs/extension-catalog.json is the enriched metadata layer for the official
extension corpus. It is defined by:
- Version tag:
schema: "pi.ext.catalog.v1"(embedded in the JSON) - Validation:
docs/extension-catalog.schema.json
Top-level fields
schema(string, const): schema identifier (pi.ext.catalog.v1)generated_at(RFC3339 string): artifact generation timestamptotal_extensions(int): number of catalog entriesitems(array): catalog entries (see below)tier_summary/runtime_summary(object): aggregate counts
Catalog entry fields (required in v1)
id(string): stable extension identifiername(string): entrypoint filename (informational)source_tier(enum): provenance tier (official/community/npm/etc)source(union): pinned source reference (git/npm/url)runtime_tier(enum): packaging shape bucket (legacy-js/multi-file/pkg-with-deps)interaction_tags(enum[]): tool/command/event/UI/provider surface tagscapabilities(enum[]): required capability set (read/write/http/exec/session/ui/etc)io_pattern(enum[]): coarse IO behavior bucketscomplexity(enum):small|medium|largefile_count/total_bytes(int): size metadata for the artifactchecksum.sha256(hex string): stable content checksum
Reserved fields (optional; populated by downstream beads)
version: extension version (when applicable; e.g. npm)license: license identifier (docs/extension-artifact-provenance.json)category_tags: workflow tags (git/tests/devops/etc)compatibility_notes: known constraints / warning reasons (seedocs/ext-compat.md)perf_budgets: perf expectations + observed baselines (bench artifacts)
Mapping / source-of-truth inputs
- Checksums + file metadata:
docs/extension-master-catalog.json - License + pinned provenance:
docs/extension-artifact-provenance.json - Node API + hostcall usage:
docs/extension-api-matrix.json - Testability notes + execution order:
docs/extension-priority.json
The purpose of coverage targets is to prevent a "high-score shortlist" from missing whole classes of real-world behavior. Targets are used by selection beads to produce a Tier-1 "must-pass" corpus that is large, stratified, and defensible.
Tier size targets (selection constraint):
- Tier-0 baseline: the upstream official example set (must-pass baseline).
- Tier-1 MUST PASS: ≥ 200 unmodified extensions, stratified across source tiers and behavior buckets.
- Tier-2 stretch: additional long-tail extensions chosen primarily for unique API surface / coverage (not popularity).
Achieved coverage (as of 2026-02-07):
All 223 validated extensions are tested. 187 pass (83.9%).
| Source tier | Target | Actual | Pass | Rate |
|---|---|---|---|---|
official-pi-mono |
60 | 61 | 60 | 98.4% |
npm-registry |
50 | 75 | 48 | 64.0% |
community |
50 | 58 | 52 | 89.7% |
third-party-github |
20 | 23 | 16 | 69.6% |
agents-mikeastock |
all | 1 | 0 | 0% |
| Total | ≥ 200 | 223 | 187 | 83.9% |
By conformance tier (complexity bucket):
| Tier | Description | Total | Pass | Rate |
|---|---|---|---|---|
| T1 | Simple single-file | 38 | 38 | 100% |
| T2 | Multi-registration | 87 | 85 | 97.7% |
| T3 | Multi-file / complex | 90 | 60 | 66.7% |
| T4 | npm dependencies | 3 | 2 | 66.7% |
| T5 | exec/network | 5 | 2 | 40.0% |
36 failures break down as:
- Manifest registration mismatch (22) — fixable by auditing manifests
- Missing npm package stub (5) — fixable by adding virtual modules
- Multi-file dependency (4) — partially fixable (needs bundling)
- Runtime error (4) — needs investigation
- Test fixture (1) — not a real extension
See tests/ext_conformance/reports/COMPATIBILITY_SUMMARY.md for the full
combined conformance + performance report.
Tier-1 behavior / capability quotas (minimum coverage buckets):
Registration / surface:
- Tools: include all extensions that register tools (or meet a minimum of 60, whichever is larger as the corpus grows).
- Event hooks: include all event-hook extensions (or ≥ 80).
- Slash commands: include ≥ 25 command extensions.
- Provider registration / streaming: include all provider-registered extensions (rare/high-risk surface).
- UI surfaces: include ≥ 15 overlay-heavy and ≥ 40 UI-integrated (header/ footer/status/message-renderer) extensions.
Hostcall / capability risk:
- Exec-heavy (
exec_api): include all (capability is high risk). - Network-heavy (
http): include ≥ 25. - FS-heavy (
read/write/edit): include ≥ 50. - Session/UI heavy (
session_api/ui_*): include ≥ 50 combined.
Category coverage (user workflow buckets):
Maintain at least a small quorum in each high-value workflow category:
git/ repo hygiene / checkpointstests/ lint / format / CIdevops/ infra / cloud toolingresearch/ search / summarizationcodegen/ refactor / scaffoldingui/ interaction / TUI enhancementssecurity/ policy / guardrails
Notes:
- These targets intentionally mix hard minima and "include-all-rare" rules. For rare-but-critical surfaces (provider registration, exec-heavy), selection should bias toward full coverage rather than sampling.
- The
docs/extension-*.jsonartifacts are the measurement source for counts and bucket classification.
Inputs
extension.json(manifest)- Source files (TS/JS or Rust/WASM)
Pipeline
- SWC build: TS/JS → bundle (tree‑shaken/minified).
- Compatibility scan: static analysis for forbidden APIs.
- Protocol shim: rewrite legacy extension imports to hostcalls.
- Artifact build:
- QuickJS bytecode (fast startup), or
- WASM component (portable + sandboxed).
- Cache by hash:
hash = sha256(manifest + bundle + engine_version)
Output
extension.artifact+artifact.json(metadata, engine, hash, caps)
This section defines the extc compiler contract that maps legacy Node/Bun imports
to PiJS shims so all 16 extensions in docs/extension-sample.json run unmodified
(no manual source edits required).
- No per-extension exceptions in extc rewrites.
- Rewrites MUST be defined solely in terms of import specifiers and generic, semantics-preserving code patterns.
- If the sample reveals a gap, fix it by adding a general rule + tests, not by branching on extension id.
Extc MUST ensure every import specifier is resolvable inside PiJS without Node/Bun.
Rewrite node:* builtins to an internal namespace that PiJS provides (so bundlers
don't externalize them):
| Source Specifier | Target Specifier |
|---|---|
node:fs |
pi:node/fs |
node:fs/promises |
pi:node/fs_promises |
node:path |
pi:node/path |
node:os |
pi:node/os |
node:url |
pi:node/url |
node:crypto |
pi:node/crypto |
node:child_process |
pi:node/child_process |
node:module |
pi:node/module |
Many real-world deps import builtins without the node: prefix. Treat these
identically:
| Source Specifier | Target Specifier |
|---|---|
fs |
pi:node/fs |
fs/promises |
pi:node/fs_promises |
path |
pi:node/path |
os |
pi:node/os |
url |
pi:node/url |
crypto |
pi:node/crypto |
child_process |
pi:node/child_process |
module |
pi:node/module |
Extc MAY inject an idempotent prelude import at the bundle entrypoint:
import 'pi:polyfills/node_globals' // installs process, Buffer, __dirname, __filename
import 'pi:polyfills/fetch' // if needed: fetch, Headers, Request, Response
import 'pi:polyfills/webassembly' // PiWasm bridge (QuickJS has no native wasm)Injection rules:
- Deterministic: stable ordering, always at the top of the entry module.
- Sourcemap-correct: injected imports MUST NOT corrupt sourcemap line mappings.
- Versioned:
shim_versionMUST be included in the artifact hash. - Idempotent: multiple injections produce identical output.
Node globals provided by pi:polyfills/node_globals:
process(withprocess.env,process.cwd(),process.platform, etc.)Buffer__dirname/__filename(computed from module URL)global(alias forglobalThis)setImmediate/clearImmediate
The compatibility scanner MUST classify APIs into:
APIs that bypass capability policy or escape the sandbox. Extc MUST reject bundles using these:
| API / Pattern | Reason |
|---|---|
require('vm') / node:vm |
Arbitrary code execution |
require('worker_threads') |
Unsupported concurrency model |
require('cluster') |
Unsupported concurrency model |
require('dgram') |
Raw UDP sockets |
require('net') (raw sockets) |
Bypasses HTTP policy |
require('tls') (raw sockets) |
Bypasses HTTP policy |
require('inspector') |
Debugger access |
require('perf_hooks') |
Performance timing oracle |
require('v8') |
Engine internals |
require('repl') |
Interactive eval |
process.binding() |
Native module access |
process.dlopen() |
Native addon loading |
Direct eval() with dynamic string |
Arbitrary code execution (see note) |
Note on new Function(...): The pinned sample includes new Function(...) for
loading a bundled script. This is flagged but allowed with evidence logging,
not forbidden outright.
Risky constructs that require evidence logging but don't block compilation:
| API / Pattern | Evidence Required |
|---|---|
new Function(...) |
Log function body hash + call site |
eval(variable) |
Log if variable is not a literal |
setTimeout(string, ...) |
Log string body hash |
setInterval(string, ...) |
Log string body hash |
Proxy / Reflect (reflection) |
Log usage pattern |
Object.defineProperty on builtins |
Log target + property |
- Extension manifest (
extension.jsonorpackage.json) - Source files (TypeScript or JavaScript)
- Optional:
tsconfig.jsonfor type resolution
-
ESM bundle: single entry module, tree-shaken, minified
-
Sourcemap: accurate line/column mapping to original source
-
Artifact metadata (
artifact.json):{ "schema": "pi.ext.artifact.v1", "extension_id": "...", "entry_module": "index.js", "hash": "sha256:...", "shim_version": "1.0.0", "rewrite_log": [ { "from": "node:fs", "to": "pi:node/fs", "locations": [...] } ], "injected_polyfills": ["pi:polyfills/node_globals"], "flagged_apis": [ { "api": "new Function", "locations": [...], "evidence_hash": "..." } ], "forbidden_apis": [], "capabilities_required": ["read", "exec"] } -
capabilities_requiredMUST be computed per §2B.3 (declared ∪ inferred) with deterministic ordering.
- Extc MUST NOT execute extension code during compilation.
- Static analysis only; no
require()resolution that triggers side effects. - If a dependency cannot be statically analyzed, emit a warning and include it verbatim (the runtime will handle capability checks).
The following Node APIs are supported via shims. Each maps to a PiJS connector with explicit capability requirements:
| Node API | Shim Module | Sync/Async | Capability | Notes |
|---|---|---|---|---|
fs.readFileSync |
pi:node/fs |
sync | read |
Blocks event loop |
fs.writeFileSync |
pi:node/fs |
sync | write |
Blocks event loop |
fs.promises.readFile |
pi:node/fs_promises |
async | read |
Preferred |
fs.promises.writeFile |
pi:node/fs_promises |
async | write |
Preferred |
fs.existsSync |
pi:node/fs |
sync | read |
|
fs.readdirSync |
pi:node/fs |
sync | read |
|
fs.statSync |
pi:node/fs |
sync | read |
|
fs.mkdirSync |
pi:node/fs |
sync | write |
|
path.join |
pi:node/path |
sync | (none) | Pure computation |
path.resolve |
pi:node/path |
sync | (none) | Uses process.cwd() |
path.dirname |
pi:node/path |
sync | (none) | Pure computation |
path.basename |
pi:node/path |
sync | (none) | Pure computation |
path.extname |
pi:node/path |
sync | (none) | Pure computation |
os.platform |
pi:node/os |
sync | env |
Returns host platform |
os.homedir |
pi:node/os |
sync | env |
Returns home directory |
os.tmpdir |
pi:node/os |
sync | env |
Returns temp directory |
child_process.spawn |
pi:node/child_process |
async | exec |
Streams stdout/stderr; supports timeout |
child_process.exec |
pi:node/child_process |
async | exec |
Buffers output; returns ChildProcess |
child_process.execFile |
pi:node/child_process |
async | exec |
Direct command execution; returns ChildProcess |
child_process.execSync |
pi:node/child_process |
sync | exec |
Blocks; prefer async |
child_process.execFileSync |
pi:node/child_process |
sync | exec |
Direct command execution |
child_process.spawnSync |
pi:node/child_process |
sync | exec |
Structured result object |
crypto.randomBytes |
pi:node/crypto |
sync | (none) | CSPRNG |
crypto.createHash |
pi:node/crypto |
sync | (none) | Pure computation |
url.parse |
pi:node/url |
sync | (none) | Pure computation |
url.URL |
pi:node/url |
sync | (none) | WHATWG URL |
process.env |
pi:polyfills/... |
sync | env |
Filtered by policy |
process.cwd() |
pi:polyfills/... |
sync | (none) | Project root |
process.exit() |
pi:polyfills/... |
sync | (none) | Throws; extension cannot exit |
Buffer.from |
pi:polyfills/... |
sync | (none) | Binary data handling |
Buffer.alloc |
pi:polyfills/... |
sync | (none) | Binary data handling |
fetch |
pi:polyfills/fetch |
async | http |
WHATWG Fetch |
Error mapping: All shim errors MUST map to the hostcall error taxonomy (§3.2):
denied, timeout, io, invalid_request, internal.
Extc MUST produce sourcemaps that:
- Map accurately: every generated line/column MUST map to the correct original source location.
- Preserve through rewrites: import rewrites MUST NOT corrupt mappings.
- Include sources: sourcemap SHOULD include
sourcesContentfor offline debugging. - Inline or external: support both inline (
//# sourceMappingURL=data:...) and external (.mapfile) formats.
Runtime usage:
- When an error occurs, the runtime MUST use the sourcemap to produce a stack trace with original file/line/column.
- Structured logs (§3.1) MUST include sourcemapped locations in
source.location.
- Unit transform fixtures: common imports + injection patterns with expected output.
- Negative tests: forbidden APIs MUST produce exact error messages.
- E2E harness: verify rewritten bundles run 16/16 sample extensions with actionable failure diagnostics.
This section defines:
- the on‑disk extension manifest (
extension.json), and - how tooling derives required capabilities deterministically from a bundle (capability inference) and merges them with the manifest.
This is the contract used by:
- extc (compiler + compatibility scanner) during artifact build (§2A), and
- runtime + harness when deciding prompt/deny and validating conformance.
Location: <extension_root>/extension.json
Fallback: if extension.json is missing, extc MAY read the same schema from
package.json#pi. In that case, name / version default to top‑level
package.json fields unless overridden inside pi. If both exist,
extension.json wins.
Canonicalization (v1):
- Manifest hashing MUST use canonical JSON (UTF‑8, no whitespace, object keys sorted lexicographically, arrays preserve order).
- The pipeline hash (§2) is computed over canonical manifest bytes.
Machine schema: docs/schema/extension_manifest.json
Schema (v1) — human‑readable form:
{
"schema": "pi.ext.manifest.v1",
"extension_id": "ext.todo",
"name": "Todo",
"version": "0.1.0",
"api_version": "1.0",
"runtime": "native-rust",
"entrypoint": "extension.native.json",
"capabilities": ["read"],
"capability_manifest": {
"schema": "pi.ext.cap.v1",
"capabilities": [
{ "capability": "read", "methods": ["tool"], "scope": { "paths": ["src/**"] } }
]
}
}Fields:
schema(required): must bepi.ext.manifest.v1.extension_id(required): stable identifier used in logs (ext.log.v1) and harness fixtures.name/version/api_version(required): must match the protocolregisterpayload (§3).runtime(required):native-rustorwasm.entrypoint(required): path relative to extension root:- Native Rust runtime: descriptor entrypoint, e.g.
extension.native.json. - WASM: component artifact path, e.g.
dist/extension.wasm.
- Native Rust runtime: descriptor entrypoint, e.g.
capabilities(optional, legacy): flat list used as a coarse capability set until all extensions emit a scoped manifest.capability_manifest(optional, recommended): scoped requirements using the schema in §3.3 (pi.ext.cap.v1).
Goal: deterministically derive the minimum known set of capabilities that an artifact appears to require, with auditable evidence.
Output: an inferred pi.ext.cap.v1‑shaped requirement set plus evidence
records. The inferred set is written into artifact.json as:
capabilities_required: a stable, sorted list of capability keys (read,write,exec,http, ...), and optionallycapability_scope_inferred: a scoped manifest when inference can extract stable scopes (paths/hosts).
Evidence sources (v1, ordered): 0) Config files (when source is unavailable):
package.json#pi.capabilitiesmay be treated as coarse evidence.- Dependency signatures MAY be used for coarse inference (e.g.,
node-fetch,axios,undici→http) withkind=config_hint.
- Import specifiers (post‑rewrite):
pi:node/fs/pi:node/fs_promises→ inferreadand/orwritebased on used APIs (see rules below).pi:node/child_process→exec.pi:polyfills/fetchorfetch(usage →http.
- PiJS primitives:
pi.tool("read"|"grep"|"find"|"ls", ...)→readpi.tool("write"|"edit", ...)→writepi.tool("bash", ...)orpi.exec(...)→execpi.http(...)→http
- Literal scope hints (best‑effort):
read/writepaths: string literals that look like relative paths.httphosts: string literals parsed as URLs; host extracted.
Inference rules (v1):
- Determinism: inference MUST be stable across platforms; ordering is:
capabilityascending, thenmethodascending, then scopes sorted. - Soundness target: inference MUST be conservative (over‑approx is allowed), but MUST NOT invent scopes from non‑literal sources. Dynamic values produce an unspecified scope (forces prompt/deny depending on policy).
- JS vs WASM: the capability names and scope semantics are identical (§3.2A).
WASM inference MAY be based on:
- static analysis of the component (if available), or
- observed
host_calltraces in capture mode (preferred for correctness).
Define:
declared: fromextension.json.capability_manifestif present, otherwise from legacyextension.json.capabilities(coarse).inferred: from the inference engine (§2B.2).overrides: user policy overrides (allow/deny/narrow scope) from config.
Effective requirements (v1):
- Start with
declared ∪ inferred(union by capability key). - Apply user deny overrides:
- Removing a capability is allowed; runtime hostcalls will return
denied. - Narrowing scope is allowed; apply scope intersection.
- If a declared capability is denied:
strict→ registration failsprompt→ user decision requiredpermissive→ allow but log
- Removing a capability is allowed; runtime hostcalls will return
- Apply user allow overrides (add capability / widen scope).
- Emit a
capability.resolvelog (see §2B.5) with the full breakdown.
The runtime/harness MUST reject an extension manifest when:
schemais unknown.name/version/api_versionare empty.- A declared capability key is unknown to the taxonomy (§3.2A).
- A declared scope contains invalid shapes (non‑string items) or non‑normalized patterns (implementation-defined, but MUST be deterministic).
At extension load (artifact or dev), the host MUST emit one log entry:
event:capability.resolvedata: declared/inferred/overrides/effective, plus evidence hashes.
Example:
{
"schema": "pi.ext.log.v1",
"ts": "2026-02-03T00:00:00Z",
"level": "info",
"event": "capability.resolve",
"message": "Resolved effective capabilities",
"correlation": { "extension_id": "ext.todo", "scenario_id": "scn-local" },
"data": {
"declared": ["read"],
"inferred": ["read", "http"],
"effective": ["read", "http"],
"evidence": [
{ "capability": "http", "kind": "literal_url", "value_hash": "sha256:..." }
]
}
}Notes:
- Evidence values SHOULD be hashed (and optionally include a redacted preview) to avoid leaking secrets; follow the redaction rules in §3.1.
- Unit fixtures for inference with deterministic ordering (same input → same inferred output).
- Negative fixtures for invalid manifests (unknown capability, invalid scope).
- Harness fixtures asserting
capability.resolvelogs are stable after normalization.
All communication uses a versioned, JSON‑encoded protocol:
docs/schema/extension_protocol.json.
Core message types:
registertool_call/tool_resultslash_command/slash_resultevent_hookhost_call/host_result(extension → core connector calls)log/error
WASM components use the WIT interface in docs/wit/extension.wit.
All extension-related logs across capture, harness, and runtime must
use the same JSONL schema. The protocol log message payload matches this
schema exactly. One log entry per line.
Log entry schema (required fields marked *):
{
"schema": "pi.ext.log.v1", // *
"ts": "2026-02-03T03:01:02.123Z", // * RFC3339
"level": "info", // * debug|info|warn|error
"event": "tool_call.start", // * stable event name
"message": "tool call dispatched", // * human summary
"correlation": { // * IDs for joining logs
"extension_id": "ext.my_ext", // *
"scenario_id": "scn-001", // *
"session_id": "sess-abc123",
"run_id": "run-20260203-0001",
"artifact_id": "sha256:...",
"tool_call_id": "tool-42",
"slash_command_id": "slash-7",
"event_id": "evt-9",
"host_call_id": "host-13",
"rpc_id": "rpc-55",
"trace_id": "trace-...",
"span_id": "span-..."
},
"source": { // optional emitter info
"component": "runtime", // capture|harness|runtime|extension
"host": "host.name",
"pid": 4242
},
"data": { "duration_ms": 12 }
}Event naming (examples):
extension.register,extension.readytool_call.start,tool_call.endslash_command.start,slash_command.endevent_hook.start,event_hook.endhost_call.start,host_call.endpolicy.decision,compat.warning
Correlation rules:
extension_id+scenario_idare required for all extension logs.- Populate the most specific ID available (
tool_call_id,slash_command_id,event_id,host_call_id,rpc_id). trace_id/span_idare optional but recommended for long chains.
Redaction rules (mandatory):
- Replace secrets/credentials with
"[REDACTED]". - Always redact keys matching (case-insensitive):
api_key,token,authorization,cookie,password,secret,private_key,credential,bearer. - For PII (email/phone/address), either redact or hash.
- Never log full file contents; log only sizes/paths/summary.
Normalization for fixtures (deterministic diffs):
- Replace
ts,pid,host,run_id,session_id,artifact_id,trace_id,span_idwith placeholders. - Normalize absolute paths to
<cwd>/.... - Stable IDs (like
scenario_id) must be deterministic and not randomized.
Log sinks (documented contract):
- Runtime:
~/.pi/agent/logs/extensions/<session_id>.jsonl(override withPI_EXTENSION_LOG_DIR). - Capture:
tests/ext_conformance/capture/<ext>/<scenario>/extension.log.jsonl - Harness:
target/ext_conformance/logs/<scenario_id>.jsonl
CI consumption:
- CI should archive
target/ext_conformance/logs/**as artifacts. - Harness compares normalized logs to fixtures; diffs are triaged by
eventandcorrelationIDs.
host_call is the only way an extension requests privileged I/O from core.
Every call is explicit, capability-gated, and logged.
host_call.payload fields (v1):
call_id(string, required): correlates request ↔ response(s).capability(string, required): the capability key evaluated by policy. MUST match the capability core derives frommethod+params(prevents spoofing).method(string, required): connector method name (e.g.tool,exec,http,session,ui,log).params(object, required): method-specific parameters.timeout_ms(int, optional): wall-clock timeout for the host operation.cancel_token(string, optional): idempotent cancellation handle (future).context(object, optional): free-form metadata (never used for policy decisions).
Example (tool call):
{
"call_id": "host-1",
"capability": "read",
"method": "tool",
"params": { "name": "grep", "input": { "pattern": "TODO", "path": "src/" } },
"timeout_ms": 2500
}Capability derivation (core-defined, v1):
- For
method="tool", required capability is derived fromparams.name:read|grep|find|ls→readwrite|edit→writebash→exec- unknown tool →
tool(forces prompt/deny depending on policy)
- For other methods, required capability is the method itself (
http,exec, etc).
host_result.payload fields (v1):
call_id(string, required)output(object, required): method-specific result object (may be empty on error)is_error(bool, required)error(object, optional): required whenis_error=true, forbidden otherwisechunk(object, optional): streaming metadata (when results are chunked)
Error example:
{
"call_id": "host-1",
"output": {},
"is_error": true,
"error": {
"code": "denied",
"message": "capability denied by policy",
"retryable": false,
"details": { "capability": "exec" }
}
}Error taxonomy (v1):
timeout: deadline reached.denied: capability not granted or out of scope.io: connector I/O failure (fs/network/process).invalid_request: malformed method/params/capability mismatch.internal: bug or invariant violation in the host.
Streaming contract (v1):
- Core may emit multiple
host_resultmessages with the samecall_id. - When streaming, each message includes
chunk.indexstarting at 0 and increasing by 1, andchunk.is_last=truemarks the final chunk. chunk.backpressureis reserved for future flow-control hints.
This section defines a single, coherent capability model that applies equally to PiJS (JS) and WASM extensions. Policy evaluation, logging, and tooling must not diverge by runtime.
| Capability | JS surface (PiJS) | WASM hostcall | Scope | Notes |
|---|---|---|---|---|
read |
pi.tool(read/grep/find/ls); pi.fs.read/list/stat |
host_call(method=tool, name in {read,grep,find,ls}); host_call(method=fs, op in {read,list,stat}) |
paths |
Path scope enforced by connector. |
write |
pi.tool(write/edit); pi.fs.write/mkdir/delete |
host_call(method=tool, name in {write,edit}); host_call(method=fs, op in {write,mkdir,delete}) |
paths |
Includes mutation; default-deny in strict mode. |
exec |
pi.exec(...); pi.tool(bash) |
host_call(method=exec); host_call(method=tool, name=bash) |
none | Process execution; high-risk. |
http |
pi.http(request) |
host_call(method=http) |
hosts |
Host allow-list enforced. |
session |
pi.session.* |
host_call(method=session) |
none | Session metadata access. |
ui |
pi.ui.* |
host_call(method=ui) |
none | May be denied in non-interactive mode. |
log |
pi.log(...) |
host_call(method=log) |
none | Structured logging only. |
tool |
pi.tool(<non-core>) |
host_call(method=tool, name=<non-core>) |
none | Used for unknown/custom tools; forces prompt/deny in strict/prompt modes. |
Notes:
- The
fshostcall method is optional until the FS connector lands, but when present it MUST map toread/writeexactly as shown above. - The
toolcapability is a catch-all for non-core tools; the host should prefer explicitread/write/execmapping for built-ins.
- Core derives capability from
method+params(never trust extension provided capability for authorization). - JS and WASM map to the same capability names. A policy decision made for JS must be identical for the equivalent WASM call.
- Mismatch is an error: if
host_call.payload.capabilitydisagrees with the derived capability, respond withinvalid_request.
- The same policy evaluator applies to both runtimes.
- Audit logs must include
capability,method, and the derived decision. - Recommended: include a
runtimefield inlog.data(jsorwasm) to make cross-runtime comparisons trivial.
register.payload.capability_manifest optionally declares the extension’s
required capabilities up front so policy can prompt/deny deterministically and
the harness can validate conformance.
Schema (v1):
{
"schema": "pi.ext.cap.v1",
"capabilities": [
{ "capability": "read", "methods": ["tool"], "scope": { "paths": ["src/**"] } },
{ "capability": "http", "methods": ["http"], "scope": { "hosts": ["api.github.com"] } }
]
}Fields:
capabilities[].capability: capability key (the same string used by policy andhost_call.payload.capability).capabilities[].methods(optional): restrict to a set of connector methods that may be used with this capability (defense-in-depth).capabilities[].scope(optional):paths: glob-like patterns relative to the project root/cwd.hosts: allow-list of hostnames/domains for network calls.env: allow-list of env var names (future connector).
Notes:
register.payload.capabilitiesremains the legacy, flat list; it will be treated as a coarse capability set until all extensions emit a manifest.- The manifest applies equally to JS and WASM runtimes; capability names and scope semantics are identical across both.
- Extensions SHOULD mirror the resolved set (declared ∪ inferred, §2B.3) in
capability_manifest; hosts MUST log any drift.
For every hostcall the runtime emits an append-only evidence ledger using
pi.ext.log.v1:
host_call.start: emitted immediately before dispatchhost_call.end: emitted once on completion (success, error, or timeout)
Required ledger fields (in log.data):
capability/methodparams_hash(sha256 hex)timeout_ms(if present)duration_ms(end event)is_error+error.code(end event, if error)
params_hash canonicalization (v1):
- Hash the canonical JSON serialization of:
{ "method": <method>, "params": <params> } - Canonical JSON rules: UTF-8, no whitespace, object keys sorted lexicographically, arrays preserve order.
- Never write raw
paramsto logs (hash-only) unless explicitly allowed by a fixture or debug mode.
extensions.policy.mode supports:
strict: deny by default, explicit grants required.prompt: ask once per capability.permissive: allow most; warn and log.
Suggested config (document‑only for now):
{
"extensions": {
"policy": {
"mode": "prompt",
"max_memory_mb": 256,
"default_caps": ["read", "write", "http"],
"deny_caps": ["exec", "env"]
}
}
}Capabilities are enforced per‑hostcall and logged in an audit ledger.
Pi exposes user-facing presets through extensionPolicy.profile and
--extension-policy:
safe→ strict deny-by-default.balanced→ prompt mode with safe defaults (legacy alias:standard).permissive→ allow-most, primarily for short-lived troubleshooting.
To inspect exactly why each capability is allowed/prompted/denied, run:
pi --explain-extension-policy
pi --explain-extension-policy --extension-policy safe
pi --explain-extension-policy --extension-policy balanced
PI_EXTENSION_ALLOW_DANGEROUS=1 pi --extension-policy balanced --explain-extension-policy--explain-extension-policy emits:
- the resolved profile and source (CLI/env/config/default),
- per-capability decisions with reasons,
- exact CLI and
settings.jsonremediation snippets.
Recommended rollout order:
- Start in
safeand inspect decisions (pi --explain-extension-policy). - Move to
balancedto validate prompt-mode UX while dangerous caps remain denied. - Use
PI_EXTENSION_ALLOW_DANGEROUS=1only for runs that require dangerous caps. - Use
permissiveonly as a short-lived debugging override, then revert.
Local operator baseline (settings.json):
{
"extensionPolicy": {
"profile": "balanced",
"allowDangerous": false
}
}Local verification:
pi --explain-extension-policy
pi --extension-policy balanced --explain-extension-policy
PI_EXTENSION_ALLOW_DANGEROUS=1 pi --extension-policy balanced --explain-extension-policyCI baseline (default deny posture):
pi --extension-policy safe --explain-extension-policyCI opt-in job (only for suites that require dangerous capabilities):
PI_EXTENSION_ALLOW_DANGEROUS=1 pi --extension-policy balanced --explain-extension-policyRollback:
- remove
PI_EXTENSION_ALLOW_DANGEROUSfrom the environment, - set
extensionPolicy.profiletosafe, - re-run
pi --explain-extension-policyand verify dangerous capability decisions aredeny.
When dangerous capabilities are enabled, operators SHOULD capture:
- explain-policy JSON output for the exact invocation,
- structured
policy.decisionlogs for allow/prompt/deny results, - hostcall ledger entries (
host_call.start/host_call.end) for sensitive methods.
Minimum incident-ready artifact set:
- command invocation (including profile/env),
- explain-policy payload snapshot,
- stderr/stdout logs for the run,
- test/e2e summary artifact path when executed in CI.
We apply a loss‑aware, evidence‑driven model to decide capability grants.
Evidence Ledger (example):
E = { uses_fs: 0.8, uses_exec: 0.1, unsigned: 0.6, size_mb: 0.2 }
Loss matrix (risk‑averse):
| grant | deny |
-----------+-------+------+
benign | 0 | 2 |
malicious | 100 | 1 |
Decision rule: grant if expected loss is lower. This supports strict and prompt modes with mathematically traceable decisions.
This is intentionally conservative: false‑deny is cheap; false‑grant is costly.
The conformance harness validates that extensions load and register correctly
in the Rust QuickJS runtime by comparing against expected registrations from
a validated manifest (VALIDATED_MANIFEST.json).
tests/ext_conformance_generated.rs— auto-generatedconformance_test!macro invocations for all 223 extensions in the corpus.tests/ext_conformance/mod.rs— harness core: loads an extension in QuickJS, captures registrations (tools, commands, flags, providers, hooks, shortcuts), compares against the validated manifest.tests/ext_conformance/fixtures/*.json— golden fixtures for 16 representative extensions (used by differential oracle tests).VALIDATED_MANIFEST.json— ground truth from the pi-mono TS runtime (generated by loading each extension in Bun and capturing its registrations).
The conformance harness uses a differential oracle approach:
- Load each extension in the pi-mono TS runtime (Bun-based) → record registered tools, commands, hooks, flags, providers, shortcuts.
- Load the same extension in the Rust QuickJS runtime → record the same.
- Compare the two outputs. Any difference is a conformance failure.
This ensures the Rust runtime produces identical behavior to the reference implementation without coupling tests to implementation details.
# Run all 223 conformance tests
cargo test --test ext_conformance_generated --features ext-conformance -- --nocapture
# Generate full conformance report (JSONL + JSON + MD)
cargo test --test ext_conformance_generated conformance_full_report \
--features ext-conformance -- --nocapture- 187 of 223 extensions pass (83.9%)
- 100% pass rate for Tier 1 (simple single-file) extensions
- 98.4% pass rate for official pi-mono extensions (60/61; 1 test fixture)
- 30 negative tests pass (malformed/hostile extensions correctly rejected)
Reports:
tests/ext_conformance/reports/conformance_baseline.json— machine-readable baselinetests/ext_conformance/reports/conformance_summary.json— summary with failure categoriestests/ext_conformance/reports/CONFORMANCE_REPORT.md— detailed per-extension resultstests/ext_conformance/reports/COMPATIBILITY_SUMMARY.md— combined conformance + perf
The performance harness measures extension load times and event dispatch latency across the corpus, enforces budgets, and detects regressions.
tests/ext_bench_harness.rs— benchmark runner with 3 scenarios: cold load (fresh runtime), warm load (cached runtime), event dispatch.tests/perf_budgets.rs— CI-enforced budget checks that read baseline data and fail if thresholds are exceeded.BENCHMARKS.md— workflow documentation (modes, env vars, interpretation).
# Quick PR check (10 diverse extensions, 3 iterations)
PI_BENCH_MODE=pr cargo test --test ext_bench_harness --features ext-conformance -- --nocapture
# Nightly full corpus (103 safe extensions, 10 iterations)
PI_BENCH_MODE=nightly PI_BENCH_MAX=103 PI_BENCH_ITERATIONS=10 \
cargo test --test ext_bench_harness --features ext-conformance -- --nocapture| Budget | Threshold | Actual (debug) | Status |
|---|---|---|---|
| Cold load P95 (across extensions) | < 200ms | 106ms | PASS |
| Cold load per-extension P99 | < 100ms | 134ms | FAIL* |
| Warm load P95 | < 100ms | 734us | PASS |
| Warm load per-extension P99 | < 100ms | 926us | PASS |
| Event dispatch P99 (PR mode) | < 5ms | 616us | PASS |
*Debug build only; release builds are 5-10x faster (~5-10ms cold load).
| Metric | Value |
|---|---|
| Median cold load (P50) | 77ms |
| Fastest cold load | 67ms (trigger-compact) |
| Slowest cold load | 126ms (hjanuschka-plan-mode) |
| Median warm load (P50) | 333us |
| Slowest warm load | 836us (jyaunches-pi-canvas) |
| Extensions benchmarked | 100 of 103 |
Reports:
tests/perf/reports/ext_bench_baseline.json— machine-readable baselinetests/perf/reports/BASELINE_REPORT.md— per-extension breakdowntests/perf/reports/budget_summary.json— budget pass/fail summary
Compatibility scanner outputs:
- compatible (safe)
- warning (works but constrained)
- blocked (unsafe / unsupported)
The system always tries to run with warnings unless strict is set.
Extensions that rely on the following will not work in the Rust QuickJS runtime:
| Limitation | Impact | Workaround |
|---|---|---|
| npm packages without stubs | Extensions importing unlisted npm packages fail to load | Add virtual module stubs (see §8.2 for the current stub list) |
| Unbundled package-style multi-file imports | failures for layouts requiring broader package resolution (../../shared, ./dist/extension.js, etc.) |
Bundle into single file before loading |
| Native Node addons | Blocked | Use hostcalls or WASM |
| Worker threads / cluster | Blocked | Unsupported concurrency model |
Raw sockets (net/tls/dgram) |
Blocked | Use pi.http() connector |
| Manifest registration mismatches | 22 failures | Audit manifests against actual registrations |
The QuickJS runtime provides shims for common Node APIs. See §2A.6 for the full compatibility matrix. Key supported modules:
node:fs—readFileSync,writeFileSync,existsSync,readdirSync,statSync,mkdirSync,realpathSync, promises APInode:path—join,resolve,dirname,basename,extname,sepnode:os—platform,homedir,tmpdir,hostname,type,archnode:crypto—randomBytes,createHash,randomUUIDnode:url—URL,parse,fileURLToPathnode:child_process—spawn,spawnSync,exec,execFile,execSync,execFileSync(viaexeccapability)node:readline— basic interface for interactive promptsnode:module—createRequirestub
The runtime also exposes a focused Bun subset through both globalThis.Bun and
import "bun":
Bun.argvBun.file(path)(exists(),text(),arrayBuffer(),json())Bun.write(pathOrFileLike, data)Bun.which(command)Bun.spawn(command, options)/Bun.spawn([cmd, ...args], options)
Compatibility stubs are provided for Bun socket APIs, but they do not create real network connections:
Bun.connect(...)— stubbed in-memory socket emitter (no network I/O)Bun.listen(...)— stubbed in-memory server emitter (no network I/O)
For real network access, use pi.http(...) or node:http.
If an extension requires unsupported Bun APIs, keep the extension unchanged and address it via runtime compatibility work (new generic shim/connector support) or capability-governed alternative APIs.
16+ npm package stubs are provided for common third-party dependencies
(openai, adm-zip, linkedom, @sourcegraph/scip-typescript, node-pty,
chokidar, jsdom, turndown, @opentelemetry/*, etc.).
To add a new extension to the validated corpus:
-
Place the extension source under the appropriate corpus directory (e.g.,
legacy_pi_mono_code/corpus/community/). -
Validate in the TS oracle — run the extension through the Bun-based harness to capture its expected registrations:
cd tests/ext_conformance/ts_oracle bun run validate.ts /path/to/extension.ts -
Add to
VALIDATED_MANIFEST.json— merge the oracle output into the manifest so the Rust conformance test has a ground-truth comparison. -
Regenerate the conformance test — the
conformance_test!macro entries intests/ext_conformance_generated.rsare generated from the manifest. -
Run conformance — verify the extension passes:
cargo test --test ext_conformance_generated test_<extension_id> \ --features ext-conformance -- --nocapture
-
Update the catalog — add an entry to
docs/extension-catalog.jsonfollowing thepi.ext.catalog.v1schema (§1C.4).
If the extension fails conformance, classify the failure (see §1C.5 failure breakdown) and determine whether a new Node shim, npm stub, or manifest correction is needed.
- WASM component runtime (Tier A) — wasmtime integration with WIT hostcalls.
extccompiler pipeline — SWC-based TS→JS bundling + QuickJS bytecode precompilation for faster cold loads.- Expand npm stubs — add new package stubs as they appear in the corpus.
- Multi-file bundling — resolve cross-directory imports for complex extensions.
- Release build benchmarks — establish release-mode baselines (expected 5-10x faster than debug).