From 33e70b5fd08597504195c4a46523cdd020c8a6d4 Mon Sep 17 00:00:00 2001 From: cyril Date: Sat, 4 Apr 2026 16:26:45 +0200 Subject: [PATCH 1/7] 38 pi-enclave:: support custom gondolin image tags --- packages/enclave/README.md | 11 ++++++++++- packages/enclave/src/config.ts | 5 +++++ packages/enclave/src/index.ts | 2 ++ packages/enclave/src/vm.ts | 3 +++ packages/enclave/templates/pi-enclave.toml | 4 ++++ packages/enclave/test/integration.test.ts | 11 +++++++++++ 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/enclave/README.md b/packages/enclave/README.md index 0f02ca4..539657d 100644 --- a/packages/enclave/README.md +++ b/packages/enclave/README.md @@ -79,6 +79,15 @@ USER_EMAIL = { command = "git config --global user.email" } ## Configuration +### Image + +Use a custom Gondolin image when you need a different base environment or a larger root filesystem. +Build and tag the image separately, then reference it here: + +```toml +image = "pi-enclave-large:latest" +``` + ### Env vars Non-secret values available in the VM and setup scripts. Three source types: @@ -154,7 +163,7 @@ mounts = [ ### Config layering -Two locations: global (`~/.pi/agent/extensions/pi-enclave.toml` + drop-ins) and project (`.pi/enclave.toml`). Project overrides global. Packages accumulate across all layers; secrets, hosts, and env merge by key (later wins). +Two locations: global (`~/.pi/agent/extensions/pi-enclave.toml` + drop-ins) and project (`.pi/enclave.toml`). Project overrides global. `image` uses the last configured value. Packages accumulate across all layers; secrets, hosts, and env merge by key (later wins). ```toml # .pi/enclave.toml — allow all GitHub operations in this project diff --git a/packages/enclave/src/config.ts b/packages/enclave/src/config.ts index f0081fe..64d03eb 100644 --- a/packages/enclave/src/config.ts +++ b/packages/enclave/src/config.ts @@ -110,6 +110,7 @@ export type GitCredentialDef = v.InferOutput; /** Top-level enclave config file schema. */ export const EnclaveFileConfig = v.object({ enabled: v.optional(v.boolean()), + image: v.optional(v.string()), packages: v.optional(v.array(v.string())), mounts: v.optional(v.array(MountDef), []), env: v.optional(v.record(v.string(), EnvDef), {}), @@ -224,6 +225,7 @@ export function collectConfigFiles(cwd: string): { path: string; config: Enclave export function mergeConfigs(layers: EnclaveFileConfig[]): EnclaveFileConfig { const merged: EnclaveFileConfig = { enabled: undefined, + image: undefined, packages: [], mounts: [], env: {}, @@ -242,6 +244,9 @@ export function mergeConfigs(layers: EnclaveFileConfig[]): EnclaveFileConfig { if (layer.enabled !== undefined) { merged.enabled = layer.enabled; } + if (layer.image !== undefined) { + merged.image = layer.image; + } if (layer.packages) { for (const pkg of layer.packages) { if (!merged.packages!.includes(pkg)) { diff --git a/packages/enclave/src/index.ts b/packages/enclave/src/index.ts index f7d807d..9de185d 100644 --- a/packages/enclave/src/index.ts +++ b/packages/enclave/src/index.ts @@ -167,6 +167,7 @@ export default function (pi: ExtensionAPI) { // ----------------------------------------------------------------------- const localCwd = process.cwd(); const { merged, policies, hasGlobalConfig, hasProjectConfig, dropIns } = loadConfig(localCwd); + const image = merged.image; const packages = merged.packages?.length ? merged.packages : DEFAULT_PACKAGES; const extraMounts = merged.mounts ?? []; const gitCredentials = merged["git-credentials"] ?? []; @@ -236,6 +237,7 @@ export default function (pi: ExtensionAPI) { const instance = new EnclaveVM({ workspaceDir: localCwd, + image, packages, extraMounts, secrets, diff --git a/packages/enclave/src/vm.ts b/packages/enclave/src/vm.ts index 3b40f68..0ad5705 100644 --- a/packages/enclave/src/vm.ts +++ b/packages/enclave/src/vm.ts @@ -32,6 +32,8 @@ export interface ExtraMount { export interface EnclaveVMOptions { /** Host directory to mount inside the VM */ workspaceDir: string; + /** Tagged Gondolin image or asset path to boot */ + image: string | undefined; /** Alpine packages to install */ packages: string[]; /** Additional directories to mount in the VM */ @@ -232,6 +234,7 @@ export class EnclaveVM { // Create and start VM this.vm = await VM.create({ + sandbox: this.options.image ? { imagePath: this.options.image } : undefined, httpHooks, env: { ...env, diff --git a/packages/enclave/templates/pi-enclave.toml b/packages/enclave/templates/pi-enclave.toml index 04c1fd5..57b0de3 100644 --- a/packages/enclave/templates/pi-enclave.toml +++ b/packages/enclave/templates/pi-enclave.toml @@ -4,6 +4,10 @@ # Default for projects without their own .pi/enclave.toml: # enabled = false +# Optional custom Gondolin image tag or asset path. +# Build a larger image separately if you need more disk space. +# image = "pi-enclave-large:latest" + # Base packages (drop-in files in pi-enclave.d/ add more): packages = ["curl", "jq"] diff --git a/packages/enclave/test/integration.test.ts b/packages/enclave/test/integration.test.ts index ab72eb4..efe902d 100644 --- a/packages/enclave/test/integration.test.ts +++ b/packages/enclave/test/integration.test.ts @@ -120,6 +120,17 @@ describe("collectConfigFiles with drop-ins", () => { expect(result.dropIns).toEqual(["git", "github", "jj"]); }); + it("uses the last configured image", () => { + ensureGlobalConfig(); + writeFileSync(globalConfigPath(), 'image = "global:latest"\n'); + const projectDir = join(tmpDir, "project"); + mkdirSync(join(projectDir, ".pi"), { recursive: true }); + writeFileSync(join(projectDir, ".pi", "enclave.toml"), 'enabled = true\nimage = "project:latest"\n'); + + const { merged } = loadConfig(projectDir); + expect(merged.image).toBe("project:latest"); + }); + it("does not walk ancestor directories", () => { ensureGlobalConfig(); const parent = join(tmpDir, "parent"); From 612786e8852c3ee628635ed3ce494376e37b284c Mon Sep 17 00:00:00 2001 From: cyril Date: Mon, 6 Apr 2026 15:21:45 +0200 Subject: [PATCH 2/7] #38 enclave allow empty empty packages --- packages/enclave/src/config.ts | 17 ++++++----------- packages/enclave/src/index.ts | 3 +-- packages/enclave/test/integration.test.ts | 10 ++++++++++ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/enclave/src/config.ts b/packages/enclave/src/config.ts index 64d03eb..feb8b76 100644 --- a/packages/enclave/src/config.ts +++ b/packages/enclave/src/config.ts @@ -145,12 +145,6 @@ export interface ResolvedHostPolicy { graphql?: ResolvedGraphQLPolicy; } -// --------------------------------------------------------------------------- -// Default packages -// --------------------------------------------------------------------------- - -export const DEFAULT_PACKAGES = ["git", "curl", "jq"]; - // --------------------------------------------------------------------------- // Config loading // --------------------------------------------------------------------------- @@ -447,9 +441,10 @@ export function initProjectConfig(cwd: string): boolean { } /** - * Add a package to a config file. Creates the file if needed. + * Persist a package in config so future enclave starts install it automatically. + * Creates the config file if needed. */ -export function addPackageToConfig(cwd: string, pkg: string, target: "project" | "global"): void { +export function addPackageToConfig(cwd: string, packageName: string, target: "project" | "global"): void { const configPath = target === "global" ? globalConfigPath() : projectConfigPath(cwd); let existing: Partial = {}; @@ -462,9 +457,9 @@ export function addPackageToConfig(cwd: string, pkg: string, target: "project" | // File doesn't exist or is invalid, start fresh } - const packages = existing.packages ?? [...DEFAULT_PACKAGES]; - if (!packages.includes(pkg)) { - packages.push(pkg); + const packages = existing.packages ?? []; + if (!packages.includes(packageName)) { + packages.push(packageName); } const dir = dirname(configPath); diff --git a/packages/enclave/src/index.ts b/packages/enclave/src/index.ts index 9de185d..26b2997 100644 --- a/packages/enclave/src/index.ts +++ b/packages/enclave/src/index.ts @@ -13,7 +13,6 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent"; import { - DEFAULT_PACKAGES, addPackageToConfig, ensureGlobalConfig, globalConfigPath, @@ -168,7 +167,7 @@ export default function (pi: ExtensionAPI) { const localCwd = process.cwd(); const { merged, policies, hasGlobalConfig, hasProjectConfig, dropIns } = loadConfig(localCwd); const image = merged.image; - const packages = merged.packages?.length ? merged.packages : DEFAULT_PACKAGES; + const packages = merged.packages ?? []; const extraMounts = merged.mounts ?? []; const gitCredentials = merged["git-credentials"] ?? []; // Network allowlist is derived from secret hosts (Gondolin builds the allowlist) diff --git a/packages/enclave/test/integration.test.ts b/packages/enclave/test/integration.test.ts index efe902d..25cc512 100644 --- a/packages/enclave/test/integration.test.ts +++ b/packages/enclave/test/integration.test.ts @@ -162,6 +162,16 @@ describe("addPackageToConfig", () => { const matches = content.match(/curl/g); expect(matches).toHaveLength(1); }); + + it("does not seed project config with hardcoded packages", () => { + const projectDir = join(tmpDir, "project"); + addPackageToConfig(projectDir, "ripgrep", "project"); + const content = readFileSync(join(projectDir, ".pi", "enclave.toml"), "utf-8"); + expect(content).toContain('packages = ["ripgrep"]'); + expect(content).not.toContain("curl"); + expect(content).not.toContain("jq"); + expect(content).not.toContain("git"); + }); }); describe("mount path resolution", () => { From 77003fd174c64d28aa0dd1aca10f6cd585205bd4 Mon Sep 17 00:00:00 2001 From: cyril Date: Mon, 6 Apr 2026 15:27:51 +0200 Subject: [PATCH 3/7] changesets --- .changeset/enclave-allow-empty-packages.md | 5 +++++ .changeset/enclave-custom-image.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/enclave-allow-empty-packages.md create mode 100644 .changeset/enclave-custom-image.md diff --git a/.changeset/enclave-allow-empty-packages.md b/.changeset/enclave-allow-empty-packages.md new file mode 100644 index 0000000..6cc6e5b --- /dev/null +++ b/.changeset/enclave-allow-empty-packages.md @@ -0,0 +1,5 @@ +--- +"pi-enclave": minor +--- + +Allow empty package lists in config (removes hardcoded default packages). diff --git a/.changeset/enclave-custom-image.md b/.changeset/enclave-custom-image.md new file mode 100644 index 0000000..588b76e --- /dev/null +++ b/.changeset/enclave-custom-image.md @@ -0,0 +1,5 @@ +--- +"pi-enclave": minor +--- + +Support custom Gondolin image tags via `image` config option. From a9fd160c795fce4fbea012a4351bb0f1f0057800 Mon Sep 17 00:00:00 2001 From: cyril Date: Mon, 6 Apr 2026 15:30:18 +0200 Subject: [PATCH 4/7] removed unneeded change --- packages/enclave/src/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/enclave/src/config.ts b/packages/enclave/src/config.ts index feb8b76..58571b2 100644 --- a/packages/enclave/src/config.ts +++ b/packages/enclave/src/config.ts @@ -444,7 +444,7 @@ export function initProjectConfig(cwd: string): boolean { * Persist a package in config so future enclave starts install it automatically. * Creates the config file if needed. */ -export function addPackageToConfig(cwd: string, packageName: string, target: "project" | "global"): void { +export function addPackageToConfig(cwd: string, pkg: string, target: "project" | "global"): void { const configPath = target === "global" ? globalConfigPath() : projectConfigPath(cwd); let existing: Partial = {}; @@ -458,8 +458,8 @@ export function addPackageToConfig(cwd: string, packageName: string, target: "pr } const packages = existing.packages ?? []; - if (!packages.includes(packageName)) { - packages.push(packageName); + if (!packages.includes(pkg)) { + packages.push(pkg); } const dir = dirname(configPath); From e15f604c0a649ca5b22e61125fb966a5e6943ae5 Mon Sep 17 00:00:00 2001 From: cyril Date: Mon, 6 Apr 2026 15:32:44 +0200 Subject: [PATCH 5/7] improve readme --- packages/enclave/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enclave/README.md b/packages/enclave/README.md index 539657d..4a5ff34 100644 --- a/packages/enclave/README.md +++ b/packages/enclave/README.md @@ -81,7 +81,7 @@ USER_EMAIL = { command = "git config --global user.email" } ### Image -Use a custom Gondolin image when you need a different base environment or a larger root filesystem. +Use a [custom Gondolin image](https://earendil-works.github.io/gondolin/custom-images/) when you need a different base environment or a larger root filesystem. Build and tag the image separately, then reference it here: ```toml From e3da5aab23ce35b59bc923597a40cf04895c4620 Mon Sep 17 00:00:00 2001 From: mgabor <9047995+mgabor3141@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:59:49 +0200 Subject: [PATCH 6/7] Update pi-enclave.toml --- packages/enclave/templates/pi-enclave.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/enclave/templates/pi-enclave.toml b/packages/enclave/templates/pi-enclave.toml index 57b0de3..88a7df1 100644 --- a/packages/enclave/templates/pi-enclave.toml +++ b/packages/enclave/templates/pi-enclave.toml @@ -9,7 +9,7 @@ # image = "pi-enclave-large:latest" # Base packages (drop-in files in pi-enclave.d/ add more): -packages = ["curl", "jq"] +packages = ["git", "curl", "jq"] # Environment variables available in the VM and setup scripts. # Values can be static strings, host commands, or host env vars. From d9785c05a896afa88f26b1ca2bf4d15e92f9fed9 Mon Sep 17 00:00:00 2001 From: mgabor3141 Date: Fri, 10 Apr 2026 14:09:37 +0200 Subject: [PATCH 7/7] test(pi-enclave): drop stale DEFAULT_PACKAGES test The DEFAULT_PACKAGES constant was removed in this PR, but the test that asserted its contents was left behind. It was already testing an implementation detail rather than observable behavior; the integration test 'does not seed project config with hardcoded packages' covers the real consequence. --- packages/enclave/test/config.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/enclave/test/config.test.ts b/packages/enclave/test/config.test.ts index 2bb1c80..4fdc800 100644 --- a/packages/enclave/test/config.test.ts +++ b/packages/enclave/test/config.test.ts @@ -1,6 +1,6 @@ import * as v from "valibot"; import { describe, expect, it } from "vitest"; -import { DEFAULT_PACKAGES, EnclaveFileConfig, mergeConfigs, resolveHostPolicies } from "../src/config.js"; +import { EnclaveFileConfig, mergeConfigs, resolveHostPolicies } from "../src/config.js"; describe("EnclaveFileConfig schema", () => { it("accepts empty config", () => { @@ -315,11 +315,3 @@ describe("resolveHostPolicies", () => { expect(policies.get("example.com")!.unmatched).toBe("allow"); }); }); - -describe("DEFAULT_PACKAGES", () => { - it("includes git, curl, jq", () => { - expect(DEFAULT_PACKAGES).toContain("git"); - expect(DEFAULT_PACKAGES).toContain("curl"); - expect(DEFAULT_PACKAGES).toContain("jq"); - }); -});