From 2480f73ed84ae3bc8d1bd002d725051555055ea3 Mon Sep 17 00:00:00 2001 From: Paul Milesi <33572994+PaulM5406@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:32:41 +0200 Subject: [PATCH] feat(pi): opt-in Pi extension loading via WARDEN_PI_EXTENSION_PATHS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pi runtime passes noExtensions: true to Pi's DefaultResourceLoader, so no Pi extension ever loads — including files operators drop into Pi's global extension dir (~/.pi/agent/extensions/). That makes it impossible to hook Warden's LLM calls the way Pi supports out of the box (LLM proxy routing, request tagging, tracing). Add an explicit opt-in: WARDEN_PI_EXTENSION_PATHS, a comma-separated list of extension files threaded to the loader as additionalExtensionPaths. Extension auto-discovery stays disabled (noExtensions remains true) — only the listed files load, so default runs stay hermetic. --- .../docs/src/content/docs/config/models.mdx | 14 +++++++++++++ packages/warden/src/sdk/runtimes/pi.test.ts | 21 +++++++++++++++++++ packages/warden/src/sdk/runtimes/pi.ts | 9 ++++++++ 3 files changed, 44 insertions(+) diff --git a/packages/docs/src/content/docs/config/models.mdx b/packages/docs/src/content/docs/config/models.mdx index 870a1662b..6acbc5fc5 100644 --- a/packages/docs/src/content/docs/config/models.mdx +++ b/packages/docs/src/content/docs/config/models.mdx @@ -58,6 +58,20 @@ Warden mirrors these to the native `{PROVIDER}_API_KEY` expected by each SDK at If you already have a native provider key set (e.g. `OPENAI_API_KEY`), Warden will use it directly and the `WARDEN_`-prefixed form is not required. +### Pi Extensions + +By default the Pi runtime loads **no** [Pi extensions](https://pi.dev/docs/extensions), +neither project-local nor global ones, so runs are hermetic. To load specific extension +files (for example an LLM proxy router or a tracing hook), set +`WARDEN_PI_EXTENSION_PATHS` to a comma-separated list of extension file paths: + +```yaml title="GitHub Actions" +env: + WARDEN_PI_EXTENSION_PATHS: /home/runner/warden-extensions/tracing.ts +``` + +Only the listed files are loaded; extension auto-discovery stays disabled. + ## Claude Runtime Models When `runtime = "claude"`, use the model IDs accepted by Claude Code: diff --git a/packages/warden/src/sdk/runtimes/pi.test.ts b/packages/warden/src/sdk/runtimes/pi.test.ts index 089533a08..15f9fca2d 100644 --- a/packages/warden/src/sdk/runtimes/pi.test.ts +++ b/packages/warden/src/sdk/runtimes/pi.test.ts @@ -219,6 +219,27 @@ describe('piRuntime.runSkill', () => { }); }); + it('loads no Pi extensions by default', async () => { + await piRuntime.runSkill(baseSkillRequest()); + + expect(piMocks.resourceLoaderOptions[0]).toEqual(expect.objectContaining({ noExtensions: true })); + expect(piMocks.resourceLoaderOptions[0]).not.toHaveProperty('additionalExtensionPaths'); + }); + + it('threads WARDEN_PI_EXTENSION_PATHS into the resource loader', async () => { + vi.stubEnv('WARDEN_PI_EXTENSION_PATHS', '/ext/tracing.ts, /ext/routing.ts,'); + try { + await piRuntime.runSkill(baseSkillRequest()); + } finally { + vi.unstubAllEnvs(); + } + + expect(piMocks.resourceLoaderOptions[0]).toEqual(expect.objectContaining({ + noExtensions: true, + additionalExtensionPaths: ['/ext/tracing.ts', '/ext/routing.ts'], + })); + }); + it('records Pi tool execution spans when trace capture is active', async () => { const toolResult = { role: 'toolResult', diff --git a/packages/warden/src/sdk/runtimes/pi.ts b/packages/warden/src/sdk/runtimes/pi.ts index 144f58ca3..0bd1082f0 100644 --- a/packages/warden/src/sdk/runtimes/pi.ts +++ b/packages/warden/src/sdk/runtimes/pi.ts @@ -343,6 +343,13 @@ async function promptWithTimeout( } } +function piExtensionPathsFromEnv(env: NodeJS.ProcessEnv = process.env): string[] { + return (env['WARDEN_PI_EXTENSION_PATHS'] ?? '') + .split(',') + .map((path) => path.trim()) + .filter((path) => path.length > 0); +} + async function runPiPrompt(options: PiPromptOptions): Promise { const warnings: string[] = []; bridgeWardenProviderApiKeyEnv(); @@ -350,11 +357,13 @@ async function runPiPrompt(options: PiPromptOptions): Promise { const model = resolvePiModel(options.model, modelRuntime); const settingsManager = buildSettingsManager(options.timeout, options.maxRetries); const agentDir = getAgentDir(); + const additionalExtensionPaths = piExtensionPathsFromEnv(); const resourceLoader = new DefaultResourceLoader({ cwd: options.cwd, agentDir, settingsManager, noExtensions: true, + ...(additionalExtensionPaths.length > 0 ? { additionalExtensionPaths } : {}), noSkills: true, noPromptTemplates: true, noThemes: true,