Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 123eac5

Browse filesBrowse files
authored
feat(kaos): add env parameter to exec and fix PyInstaller subprocess env (MoonshotAI#914)
1 parent 923509d commit 123eac5
Copy full SHA for 123eac5

11 files changed

+84-13Lines changed: 84 additions & 13 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎CHANGELOG.md‎

Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Only write entries that are worth mentioning to users.
1313

1414
- Web: Fix approval request states not updating when session is interrupted or cancelled
1515
- Web: Add activity status indicator showing agent state (processing, waiting for approval, etc.)
16+
- Build: Fix subprocess library path conflicts in PyInstaller-frozen builds on Linux
1617

1718
## 1.8.0 (2026-02-05)
1819

Collapse file

‎packages/kaos/CHANGELOG.md‎

Copy file name to clipboardExpand all lines: packages/kaos/CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add `env` parameter to `exec()` method for passing environment variables to subprocesses
6+
57
## 0.6.0 (2026-01-09)
68

79
- Add optional `n` parameter to `readbytes` to read only the first n bytes
Collapse file

‎packages/kaos/src/kaos/__init__.py‎

Copy file name to clipboardExpand all lines: packages/kaos/src/kaos/__init__.py
+9-4Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import contextvars
4-
from collections.abc import AsyncGenerator, AsyncIterator, Iterable
4+
from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Mapping
55
from dataclasses import dataclass
66
from pathlib import PurePath
77
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable
@@ -216,9 +216,14 @@ async def mkdir(
216216
"""Create a directory at the given path."""
217217
...
218218

219-
async def exec(self, *args: str) -> KaosProcess:
219+
async def exec(self, *args: str, env: Mapping[str, str] | None = None) -> KaosProcess:
220220
"""
221221
Execute a command with arguments and return the running process.
222+
223+
Args:
224+
*args: Command and its arguments.
225+
env: Environment variables for the subprocess. If None, inherits
226+
from the parent process.
222227
"""
223228
...
224229

@@ -337,5 +342,5 @@ async def mkdir(path: StrOrKaosPath, parents: bool = False, exist_ok: bool = Fal
337342
return await get_current_kaos().mkdir(path, parents=parents, exist_ok=exist_ok)
338343

339344

340-
async def exec(*args: str) -> KaosProcess:
341-
return await get_current_kaos().exec(*args)
345+
async def exec(*args: str, env: Mapping[str, str] | None = None) -> KaosProcess:
346+
return await get_current_kaos().exec(*args, env=env)
Collapse file

‎packages/kaos/src/kaos/local.py‎

Copy file name to clipboardExpand all lines: packages/kaos/src/kaos/local.py
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import posixpath as pathmodule
1515
from pathlib import PurePosixPath as PurePathClass
1616

17+
from collections.abc import Mapping
18+
1719
import aiofiles
1820
import aiofiles.os
1921

@@ -158,7 +160,7 @@ async def mkdir(
158160
local_path = path.unsafe_to_local_path() if isinstance(path, KaosPath) else Path(path)
159161
await asyncio.to_thread(local_path.mkdir, parents=parents, exist_ok=exist_ok)
160162

161-
async def exec(self, *args: str) -> KaosProcess:
163+
async def exec(self, *args: str, env: Mapping[str, str] | None = None) -> KaosProcess:
162164
if not args:
163165
raise ValueError("At least one argument (the program to execute) is required.")
164166

@@ -167,6 +169,7 @@ async def exec(self, *args: str) -> KaosProcess:
167169
stdin=asyncio.subprocess.PIPE,
168170
stdout=asyncio.subprocess.PIPE,
169171
stderr=asyncio.subprocess.PIPE,
172+
env=env,
170173
)
171174
return self.Process(process)
172175

Collapse file

‎packages/kaos/src/kaos/ssh.py‎

Copy file name to clipboardExpand all lines: packages/kaos/src/kaos/ssh.py
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import posixpath
44
import shlex
55
import stat
6-
from collections.abc import AsyncGenerator
6+
from collections.abc import AsyncGenerator, Mapping
77
from pathlib import PurePath, PurePosixPath
88
from typing import TYPE_CHECKING, Literal
99

@@ -273,7 +273,7 @@ async def mkdir(
273273
raise FileExistsError(f"{path} already exists")
274274
await self._sftp.mkdir(str(path))
275275

276-
async def exec(self, *args: str) -> KaosProcess:
276+
async def exec(self, *args: str, env: Mapping[str, str] | None = None) -> KaosProcess:
277277
if not args:
278278
raise ValueError("At least one argument (the program to execute) is required.")
279279
command = " ".join(shlex.quote(arg) for arg in args)
@@ -285,7 +285,7 @@ async def exec(self, *args: str) -> KaosProcess:
285285
# This is intentionally strict: if cwd doesn't exist, the command fails.
286286
if self._cwd:
287287
command = f"cd {shlex.quote(self._cwd)} && {command}"
288-
process = await self._connection.create_process(command, encoding=None)
288+
process = await self._connection.create_process(command, encoding=None, env=env)
289289
return self.Process(process)
290290

291291
async def unsafe_close(self) -> None:
Collapse file

‎src/kimi_cli/acp/kaos.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/acp/kaos.py
+3-3Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4-
from collections.abc import AsyncGenerator, Iterable
4+
from collections.abc import AsyncGenerator, Iterable, Mapping
55
from contextlib import suppress
66
from typing import Literal
77

@@ -262,8 +262,8 @@ async def mkdir(
262262
) -> None:
263263
await self._fallback.mkdir(path, parents=parents, exist_ok=exist_ok)
264264

265-
async def exec(self, *args: str) -> KaosProcess:
266-
return await self._fallback.exec(*args)
265+
async def exec(self, *args: str, env: Mapping[str, str] | None = None) -> KaosProcess:
266+
return await self._fallback.exec(*args, env=env)
267267

268268
def _abs_path(self, path: StrOrKaosPath) -> str:
269269
kaos_path = path if isinstance(path, KaosPath) else KaosPath(path)
Collapse file

‎src/kimi_cli/tools/shell/__init__.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/tools/shell/__init__.py
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from kimi_cli.tools.display import ShellDisplayBlock
1313
from kimi_cli.tools.utils import ToolRejectedError, ToolResultBuilder, load_desc
1414
from kimi_cli.utils.environment import Environment
15+
from kimi_cli.utils.subprocess_env import get_clean_env
1516

1617
MAX_TIMEOUT = 5 * 60
1718

@@ -106,7 +107,7 @@ async def _read_stream(stream: AsyncReadable, cb: Callable[[bytes], None]):
106107
else:
107108
break
108109

109-
process = await kaos.exec(*self._shell_args(command))
110+
process = await kaos.exec(*self._shell_args(command), env=get_clean_env())
110111

111112
try:
112113
await asyncio.wait_for(
Collapse file

‎src/kimi_cli/ui/shell/__init__.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/ui/shell/__init__.py
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from kimi_cli.utils.logging import open_original_stderr
2828
from kimi_cli.utils.signals import install_sigint_handler
2929
from kimi_cli.utils.slashcmd import SlashCommand, SlashCommandCall, parse_slash_command_call
30+
from kimi_cli.utils.subprocess_env import get_clean_env
3031
from kimi_cli.utils.term import ensure_new_line, ensure_tty_sane
3132
from kimi_cli.wire.types import ContentPart, StatusUpdate
3233

@@ -163,7 +164,7 @@ def _handler():
163164
kwargs: dict[str, Any] = {}
164165
if stderr is not None:
165166
kwargs["stderr"] = stderr
166-
proc = await asyncio.create_subprocess_shell(command, **kwargs)
167+
proc = await asyncio.create_subprocess_shell(command, env=get_clean_env(), **kwargs)
167168
await proc.wait()
168169
except Exception as e:
169170
logger.exception("Failed to run shell command:")
Collapse file
+52Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Utilities for subprocess environment handling.
2+
3+
This module provides utilities to handle environment variables when spawning
4+
subprocesses from a PyInstaller-frozen application. The main issue is that
5+
PyInstaller's bootloader modifies LD_LIBRARY_PATH to prioritize bundled libraries,
6+
which can cause conflicts when spawning external programs that expect system libraries.
7+
8+
See: https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import os
14+
import sys
15+
16+
# Environment variables that PyInstaller may modify on Linux
17+
_PYINSTALLER_LD_VARS = [
18+
"LD_LIBRARY_PATH",
19+
"LD_PRELOAD",
20+
]
21+
22+
23+
def get_clean_env(base_env: dict[str, str] | None = None) -> dict[str, str]:
24+
"""
25+
Get a clean environment suitable for spawning subprocesses.
26+
27+
In a PyInstaller-frozen application on Linux, this function restores
28+
the original library path environment variables, preventing subprocesses
29+
from loading incompatible bundled libraries.
30+
31+
Args:
32+
base_env: Base environment to start from. If None, uses os.environ.
33+
34+
Returns:
35+
A dictionary of environment variables safe for subprocess use.
36+
"""
37+
env = dict(base_env if base_env is not None else os.environ)
38+
39+
# Only process in PyInstaller frozen environment on Linux
40+
if not getattr(sys, "frozen", False) or sys.platform != "linux":
41+
return env
42+
43+
for var in _PYINSTALLER_LD_VARS:
44+
orig_key = f"{var}_ORIG"
45+
if orig_key in env:
46+
# Restore the original value that was saved by PyInstaller bootloader
47+
env[var] = env[orig_key]
48+
elif var in env:
49+
# Variable was not set before PyInstaller modified it, so remove it
50+
del env[var]
51+
52+
return env
Collapse file

‎src/kimi_cli/web/api/sessions.py‎

Copy file name to clipboardExpand all lines: src/kimi_cli/web/api/sessions.py
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from kimi_cli.metadata import load_metadata, save_metadata
2424
from kimi_cli.session import Session as KimiCLISession
25+
from kimi_cli.utils.subprocess_env import get_clean_env
2526
from kimi_cli.web.auth import is_origin_allowed, is_private_ip, verify_token
2627
from kimi_cli.web.models import (
2728
GenerateTitleRequest,
@@ -961,6 +962,7 @@ async def get_session_git_diff(session_id: UUID) -> GitDiffStats:
961962
cwd=str(work_dir),
962963
stdout=asyncio.subprocess.DEVNULL,
963964
stderr=asyncio.subprocess.DEVNULL,
965+
env=get_clean_env(),
964966
)
965967
await check_proc.wait()
966968
has_head = check_proc.returncode == 0
@@ -975,6 +977,7 @@ async def get_session_git_diff(session_id: UUID) -> GitDiffStats:
975977
cwd=str(work_dir),
976978
stdout=asyncio.subprocess.PIPE,
977979
stderr=asyncio.subprocess.PIPE,
980+
env=get_clean_env(),
978981
)
979982
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5.0)
980983

@@ -1012,6 +1015,7 @@ async def get_session_git_diff(session_id: UUID) -> GitDiffStats:
10121015
cwd=str(work_dir),
10131016
stdout=asyncio.subprocess.PIPE,
10141017
stderr=asyncio.subprocess.DEVNULL,
1018+
env=get_clean_env(),
10151019
)
10161020
untracked_stdout, _ = await asyncio.wait_for(untracked_proc.communicate(), timeout=5.0)
10171021

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.