From 41fbd08e2a8f4169c9e5071fd7308cd0986743fd Mon Sep 17 00:00:00 2001 From: Victor Savkov Date: Sat, 28 Feb 2026 18:34:05 -0500 Subject: [PATCH] =?UTF-8?q?If=20pi=20is=20launched=20via=20the=20snap-pack?= =?UTF-8?q?aged=20node=20runtime,=20the=20snap=20wrapper=20(/snap/bin/node?= =?UTF-8?q?=20=E2=86=92=20/usr/bin/snap)=20applies=20AppArmor=20confinemen?= =?UTF-8?q?t.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When pi (a snap process) tried to spawn another pi subprocess, snap's confinement silently brakes the stdout pipe — the child process ran but produced no readable output, always returning empty results. Use process.execPath (the real node binary that snap internally uses) and invoke the pi CLI script directly, to bypass the snap wrapper and its pipe-breaking confinement. --- extensions/agent-chain.ts | 6 +++++- extensions/agent-team.ts | 7 ++++++- extensions/pi-pi.ts | 6 +++++- extensions/subagent-widget.ts | 10 ++++++++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/extensions/agent-chain.ts b/extensions/agent-chain.ts index 8cf7d2a..e50776c 100644 --- a/extensions/agent-chain.ts +++ b/extensions/agent-chain.ts @@ -25,6 +25,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { spawn } from "child_process"; +import { realpathSync } from "fs"; import { readFileSync, existsSync, readdirSync, mkdirSync, unlinkSync } from "fs"; import { join, resolve } from "path"; import { applyExtensionDefaults } from "./themeMap.ts"; @@ -364,7 +365,10 @@ export default function (pi: ExtensionAPI) { const state = stepStates[stepIndex]; return new Promise((resolve) => { - const proc = spawn("pi", args, { + // Use process.execPath (real node) + pi script to bypass snap AppArmor confinement. + const nodeBin = process.execPath; + const piScript = (() => { try { return realpathSync(process.argv[1]); } catch { return process.argv[1]; } })(); + const proc = spawn(nodeBin, [piScript, ...args], { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env }, }); diff --git a/extensions/agent-team.ts b/extensions/agent-team.ts index 66ecbef..87a4c3e 100644 --- a/extensions/agent-team.ts +++ b/extensions/agent-team.ts @@ -21,6 +21,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { Text, type AutocompleteItem, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { spawn } from "child_process"; +import { realpathSync } from "fs"; import { readdirSync, readFileSync, existsSync, mkdirSync, unlinkSync } from "fs"; import { join, resolve } from "path"; import { applyExtensionDefaults } from "./themeMap.ts"; @@ -365,7 +366,11 @@ export default function (pi: ExtensionAPI) { const textChunks: string[] = []; return new Promise((resolve) => { - const proc = spawn("pi", args, { + // Use process.execPath (real node binary) + resolved pi script to bypass + // snap AppArmor confinement that breaks pipes on snap-to-snap spawns. + const nodeBin = process.execPath; + const piScript = (() => { try { return realpathSync(process.argv[1]); } catch { return process.argv[1]; } })(); + const proc = spawn(nodeBin, [piScript, ...args], { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env }, }); diff --git a/extensions/pi-pi.ts b/extensions/pi-pi.ts index 97c46d2..eb38816 100644 --- a/extensions/pi-pi.ts +++ b/extensions/pi-pi.ts @@ -19,6 +19,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; import { spawn } from "child_process"; +import { realpathSync } from "fs"; import { readdirSync, readFileSync, existsSync, mkdirSync } from "fs"; import { join, resolve } from "path"; import { applyExtensionDefaults } from "./themeMap.ts"; @@ -291,7 +292,10 @@ export default function (pi: ExtensionAPI) { const textChunks: string[] = []; return new Promise((resolve) => { - const proc = spawn("pi", args, { + // Use process.execPath (real node) + pi script to bypass snap AppArmor confinement. + const nodeBin = process.execPath; + const piScript = (() => { try { return realpathSync(process.argv[1]); } catch { return process.argv[1]; } })(); + const proc = spawn(nodeBin, [piScript, ...args], { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env }, }); diff --git a/extensions/subagent-widget.ts b/extensions/subagent-widget.ts index a31ac6e..89489db 100644 --- a/extensions/subagent-widget.ts +++ b/extensions/subagent-widget.ts @@ -16,7 +16,8 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { DynamicBorder } from "@mariozechner/pi-coding-agent"; import { Container, Text } from "@mariozechner/pi-tui"; import { Type } from "@sinclair/typebox"; -const { spawn } = require("child_process") as any; +import { spawn } from "child_process"; +import { realpathSync } from "fs"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; @@ -139,7 +140,12 @@ export default function (pi: ExtensionAPI) { : "openrouter/google/gemini-3-flash-preview"; return new Promise((resolve) => { - const proc = spawn("pi", [ + // Use process.execPath (real node binary, not snap wrapper) + resolved pi script + // to bypass snap AppArmor confinement that breaks pipes on snap-to-snap spawns. + const nodeBin = process.execPath; + const piScript = (() => { try { return realpathSync(process.argv[1]); } catch { return process.argv[1]; } })(); + const proc = spawn(nodeBin, [ + piScript, "--mode", "json", "-p", "--session", state.sessionFile, // persistent session for /subcont resumption