Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion registry/agent/pi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
}
},
"scripts": {
"build": "tsc",
"build": "tsc && node scripts/build-snapshot-bundle.mjs",
"build:snapshot": "node scripts/build-snapshot-bundle.mjs",
"check-types": "tsc --noEmit"
},
"dependencies": {
Expand Down
121 changes: 121 additions & 0 deletions registry/agent/pi/scripts/build-snapshot-bundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Builds the Pi SDK snapshot bundle (Step 2a).
*
* Bundles src/snapshot-entry.ts into a single IIFE at dist/pi-sdk-snapshot.js that
* evaluates the SDK graph and publishes it on globalThis.__PI_SDK_RUNTIME__. node:
* builtins stay external (provided by the V8 runtime's bridge polyfills, already in
* the snapshot heap); heavy provider SDKs reached only via dynamic import() stay
* external so they remain lazy and load post-restore from the VFS.
*
* The build env intentionally clears DISPLAY/WAYLAND_DISPLAY (C0 mitigation): the
* optional @mariozechner/clipboard NAPI addon is behind a DISPLAY guard; with it
* unset the SDK bakes `clipboard = null` so no native pointer enters the snapshot.
*/
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { pathToFileURL } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const pkgRoot = join(here, "..");

// esbuild lives in the workspace pnpm store; resolve it from there.
const require = createRequire(import.meta.url);
const repoRoot = join(pkgRoot, "..", "..", "..");
let esbuildPath;
try {
esbuildPath = require.resolve("esbuild", { paths: [pkgRoot, repoRoot] });
} catch {
const { globSync } = await import("node:fs");
const matches = globSync(
join(repoRoot, "node_modules/.pnpm/esbuild@*/node_modules/esbuild/lib/main.js"),
);
if (matches.length === 0) throw new Error("esbuild not found in workspace");
esbuildPath = matches.sort().reverse()[0];
}
const { build } = await import(pathToFileURL(esbuildPath).href);
const outfile = join(pkgRoot, "dist", "pi-sdk-snapshot.js");

// Provider SDKs the pi-ai layer pulls only via dynamic import(); keep them lazy.
const lazyExternals = [
"@anthropic-ai/sdk",
"openai",
"@google/genai",
"@mistralai/mistralai",
"@aws-sdk/client-bedrock-runtime",
"proxy-agent",
"@mariozechner/clipboard",
];

// The adapter deliberately imports deep `dist/core/*` paths to skip the SDK's TUI
// graph, but the package `exports` map only exposes `.`/`./hooks`. Mirror the
// adapter: resolve the deep specifiers to absolute file paths under the package's
// dist dir, bypassing the exports map (esbuild bundles absolute paths fine).
const { realpathSync, existsSync } = await import("node:fs");
const piCodingRoot = [
join(pkgRoot, "node_modules/@mariozechner/pi-coding-agent"),
join(repoRoot, "node_modules/@mariozechner/pi-coding-agent"),
].find((p) => existsSync(p));
if (!piCodingRoot) throw new Error("pi-coding-agent not found in node_modules");
const piCodingReal = realpathSync(piCodingRoot);
const piCodingDist = join(piCodingReal, "dist");
// pnpm sibling layout: pi-coding-agent's own deps (incl. pi-agent-core) live in the
// same `.pnpm/<hash>/node_modules/@mariozechner` dir. Resolve transitive
// @mariozechner/* deps from there so we bundle exactly what the SDK links against.
const piSiblingScope = dirname(piCodingReal); // .../node_modules/@mariozechner
const deepImportPlugin = {
name: "pi-deep-imports",
setup(b) {
b.onResolve({ filter: /^@mariozechner\/pi-coding-agent\/dist\// }, (a) => {
const rel = a.path.replace("@mariozechner/pi-coding-agent/dist/", "");
return { path: join(piCodingDist, rel) };
});
b.onResolve({ filter: /^@mariozechner\/pi-agent-core/ }, (a) => {
const sub = a.path.replace("@mariozechner/pi-agent-core", "") || "/dist/index.js";
return { path: join(piSiblingScope, "pi-agent-core", sub) };
});
},
};

// esbuild stubs `import.meta` to `{}` in IIFE output, so import.meta.url is
// undefined and the SDK's top-level install-detection (fileURLToPath(import.meta.url))
// crashes. Pin it to a single deterministic URL: the SDK's projected guest path, so
// any top-level dir/version computation resolves to where the SDK actually lives in
// the VM. Override with PI_SNAPSHOT_BASE_URL (e.g. the real host path for testing).
const baseUrl =
process.env.PI_SNAPSHOT_BASE_URL ||
"file:///root/node_modules/@mariozechner/pi-coding-agent/dist/index.js";

const result = await build({
entryPoints: [join(pkgRoot, "src", "snapshot-entry.ts")],
outfile,
bundle: true,
format: "iife",
platform: "node", // node: builtins become external require() calls
target: "esnext",
external: lazyExternals,
plugins: [deepImportPlugin],
define: {
"import.meta.url": JSON.stringify(baseUrl),
// C0 mitigation: force the headless path in the clipboard native-addon
// guard so `require("@mariozechner/clipboard")` (a NAPI addon) is never
// triggered at snapshot-eval time, regardless of the sidecar's env.
"process.env.DISPLAY": '""',
"process.env.WAYLAND_DISPLAY": '""',
"process.env.TERMUX_VERSION": '""',
},
legalComments: "none",
logLevel: "info",
metafile: true,
});

const bytes = readFileSync(outfile);
const sha256 = createHash("sha256").update(bytes).digest("hex");
writeFileSync(`${outfile}.sha256`, `${sha256}\n`);

const inputs = Object.keys(result.metafile.inputs).length;
console.log(
`\npi-sdk-snapshot.js: ${(bytes.length / 1024).toFixed(0)} KiB · ${inputs} modules inlined · sha256 ${sha256.slice(0, 12)}…`,
);
63 changes: 63 additions & 0 deletions registry/agent/pi/src/snapshot-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Pi SDK snapshot entry (Step 2a/2c — eval/entry split).
*
* This module is bundled by esbuild into a single IIFE (`dist/pi-sdk-snapshot.js`)
* whose ONLY job is to evaluate the Pi SDK module graph and publish its exports on
* a well-known global, `globalThis.__PI_SDK_RUNTIME__`. The bundle is run once at V8
* snapshot-creation time; the evaluated SDK heap is captured into the startup blob
* and reused to seed every fresh per-session isolate. After restore the adapter
* reads the global instead of dynamically importing the SDK from the guest VFS,
* collapsing the ~830ms per-session module-load/eval tax.
*
* INVARIANTS (enforced by the C0 snapshot-safety scan — see
* ~/.agents/research/pi-snapshot-safety-scan.md):
* - This entry performs NO per-session work: it must not read cwd, model,
* ANTHROPIC env vars, HOME, argv, open fds/sockets, or start timers. It only
* binds the SDK's static exports. All per-session configuration is injected
* post-restore by the adapter (newSession), exactly as before.
* - Heavy provider SDKs (@anthropic-ai/sdk, openai, …) are loaded by the SDK via
* dynamic import() and are intentionally NOT part of this static graph; they
* stay lazy and load post-restore from the VFS on first prompt.
* - The bundle is keyed by the sha256 of its own (fully inlined) bytes, so any SDK
* dependency change invalidates the snapshot.
*/

import { Agent } from "@mariozechner/pi-agent-core";
import { AuthStorage } from "@mariozechner/pi-coding-agent/dist/core/auth-storage.js";
import {
getAgentDir,
getDocsPath,
} from "@mariozechner/pi-coding-agent/dist/config.js";
import { DEFAULT_THINKING_LEVEL } from "@mariozechner/pi-coding-agent/dist/core/defaults.js";
import { convertToLlm } from "@mariozechner/pi-coding-agent/dist/core/messages.js";
import { ModelRegistry } from "@mariozechner/pi-coding-agent/dist/core/model-registry.js";
import { DefaultResourceLoader } from "@mariozechner/pi-coding-agent/dist/core/resource-loader.js";
import {
createAgentSession,
createCodingTools,
} from "@mariozechner/pi-coding-agent/dist/core/sdk.js";
import { SessionManager } from "@mariozechner/pi-coding-agent/dist/core/session-manager.js";
import { SettingsManager } from "@mariozechner/pi-coding-agent/dist/core/settings-manager.js";
import { createAllTools } from "@mariozechner/pi-coding-agent/dist/core/tools/index.js";

// The shape mirrors `PiSdkRuntime` in adapter.ts so the adapter can read the
// global with no behavioral difference from the dynamic-import path.
const runtime = {
Agent,
AuthStorage,
DefaultResourceLoader,
DEFAULT_THINKING_LEVEL,
ModelRegistry,
SettingsManager,
SessionManager,
convertToLlm,
getAgentDir,
getDocsPath,
createAgentSession,
createCodingTools,
createAllTools,
};

// Publish on the global so it survives into every fresh context cloned from the
// snapshotted default context.
(globalThis as Record<string, unknown>).__PI_SDK_RUNTIME__ = runtime;
2 changes: 1 addition & 1 deletion registry/agent/pi/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "src/snapshot-entry.ts"]
}
Loading