Skip to content
Open
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
57 changes: 57 additions & 0 deletions apps/server/src/terminal/Layers/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,63 @@ it.layer(
}),
);

it.effect("strips AppImage runtime env from terminal sessions", () =>
Effect.gen(function* () {
const appDir = "/tmp/.mount_T3Codeabc123";
const { manager, ptyAdapter } = yield* createManager(5, {
env: {
APPIMAGE: "/home/user/T3-Code.AppImage",
APPDIR: appDir,
ARGV0: "/home/user/T3-Code.AppImage",
OWD: "/home/user/project",
PATH: `${appDir}/usr/bin:${appDir}:/usr/local/bin:/usr/bin:/bin`,
LD_LIBRARY_PATH: `${appDir}/usr/lib:/home/user/.local/lib`,
TEST_TERMINAL_KEEP: "keep-me",
},
});
yield* manager.open(openInput());
const spawnInput = ptyAdapter.spawnInputs[0];
expect(spawnInput).toBeDefined();
if (!spawnInput) return;

// AppImage runtime markers must never reach the PTY — tools inside the
// terminal otherwise resolve against the AppImage mount (e.g. PHP_BINARY
// reporting the AppImage path instead of the real binary).
expect(spawnInput.env.APPIMAGE).toBeUndefined();
expect(spawnInput.env.APPDIR).toBeUndefined();
expect(spawnInput.env.ARGV0).toBeUndefined();
expect(spawnInput.env.OWD).toBeUndefined();
// PATH/LD_LIBRARY_PATH keep the user's real entries but drop the AppImage
// mount segments that the runtime prepended.
expect(spawnInput.env.PATH).toBe("/usr/local/bin:/usr/bin:/bin");
expect(spawnInput.env.LD_LIBRARY_PATH).toBe("/home/user/.local/lib");
// Unrelated host vars still pass through untouched.
expect(spawnInput.env.TEST_TERMINAL_KEEP).toBe("keep-me");
}),
);

it.effect("leaves the environment untouched when not launched from an AppImage", () =>
Effect.gen(function* () {
const { manager, ptyAdapter } = yield* createManager(5, {
env: {
PATH: "/usr/local/bin:/usr/bin:/bin",
LD_LIBRARY_PATH: "/home/user/.local/lib",
// Without APPIMAGE/APPDIR set, OWD is an ordinary variable and must
// not be stripped — only an AppImage launch gives it special meaning.
OWD: "/home/user/keep-this",
},
});
yield* manager.open(openInput());
const spawnInput = ptyAdapter.spawnInputs[0];
expect(spawnInput).toBeDefined();
if (!spawnInput) return;

expect(spawnInput.env.PATH).toBe("/usr/local/bin:/usr/bin:/bin");
expect(spawnInput.env.LD_LIBRARY_PATH).toBe("/home/user/.local/lib");
expect(spawnInput.env.OWD).toBe("/home/user/keep-this");
}),
);

it.effect("injects runtime env overrides into spawned terminals", () =>
Effect.gen(function* () {
const { manager, ptyAdapter } = yield* createManager();
Expand Down
49 changes: 48 additions & 1 deletion apps/server/src/terminal/Layers/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,53 @@ function shouldExcludeTerminalEnvKey(key: string): boolean {
return TERMINAL_ENV_BLOCKLIST.has(normalizedKey);
}

// Marker variables the AppImage runtime injects into the process it launches.
// They describe the AppImage itself, not the user's session, so terminals must
// not inherit them.
const APPIMAGE_RUNTIME_ENV_KEYS = ["APPIMAGE", "APPDIR", "ARGV0", "OWD"] as const;
// PATH-style variables the AppImage runtime prepends with its temporary mount
// (e.g. /tmp/.mount_T3-XXXX/usr/bin). Only the mount segments are dropped; the
// user's real entries are preserved.
const APPIMAGE_PATH_LIKE_ENV_KEYS = ["PATH", "LD_LIBRARY_PATH"] as const;

function isPathSegmentUnderAppDir(segment: string, appDir: string): boolean {
return segment === appDir || segment.startsWith(`${appDir}/`);
}

// On Linux AppImage builds the runtime mounts the app under a temporary dir and
// injects APPIMAGE/APPDIR/ARGV0/OWD plus mount entries on PATH/LD_LIBRARY_PATH.
// The integrated terminal inherits the server process environment, so without
// this scrub those leak into the PTY and tools resolve against the AppImage
// mount instead of the user's real environment (e.g. `php` reporting
// PHP_BINARY as the AppImage path). See issue #1699. The scrub is gated on an
// actual AppImage launch so non-AppImage environments are left untouched.
function stripAppImageRuntimeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
if (env.APPIMAGE === undefined && env.APPDIR === undefined) return env;

const scrubbed: NodeJS.ProcessEnv = { ...env };
for (const key of APPIMAGE_RUNTIME_ENV_KEYS) {
delete scrubbed[key];
}

const appDir = env.APPDIR?.replace(/\/+$/, "");
if (appDir) {
for (const key of APPIMAGE_PATH_LIKE_ENV_KEYS) {
const value = scrubbed[key];
if (value === undefined) continue;
const kept = value
.split(":")
.filter((segment) => segment.length > 0 && !isPathSegmentUnderAppDir(segment, appDir));
if (kept.length > 0) {
scrubbed[key] = kept.join(":");
} else {
delete scrubbed[key];
}
}
}

return scrubbed;
}

function createTerminalSpawnEnv(
baseEnv: NodeJS.ProcessEnv,
runtimeEnv?: Record<string, string> | null,
Expand All @@ -960,7 +1007,7 @@ function createTerminalSpawnEnv(
spawnEnv[key] = value;
}
}
return spawnEnv;
return stripAppImageRuntimeEnv(spawnEnv);
}

function normalizedRuntimeEnv(
Expand Down
Loading