flowent 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ from flowent.approval import (
12
12
  )
13
13
  from flowent.patch import affected_paths
14
14
  from flowent.sandbox import SandboxError, SandboxRunner, path_is_within
15
+ from flowent.shell import shell_invocation
15
16
  from flowent.tools import (
16
17
  ToolContext,
17
18
  ToolResult,
@@ -290,10 +291,11 @@ async def shell_command_with_writable_paths(
290
291
  ) -> ToolResult:
291
292
  command = str(arguments["command"])
292
293
  timeout_seconds = number_argument(arguments, "timeout_seconds", 30)
294
+ invocation = shell_invocation(command)
293
295
  result = await SandboxRunner(
294
296
  cwd=context.cwd,
295
297
  writable_roots=writable_paths,
296
- ).run_async(["/bin/sh", "-c", command], timeout_seconds=timeout_seconds)
298
+ ).run_async(invocation.args, env=invocation.env, timeout_seconds=timeout_seconds)
297
299
  ok = result.exit_code == 0
298
300
  content = result.stdout or result.stderr
299
301
  return ToolResult(
@@ -355,8 +357,9 @@ async def shell_command_without_sandbox(
355
357
  ) -> ToolResult:
356
358
  command = str(arguments["command"])
357
359
  timeout_seconds = number_argument(arguments, "timeout_seconds", 30)
360
+ invocation = shell_invocation(command)
358
361
  result = await SandboxRunner(cwd=context.cwd).run_unsandboxed_async(
359
- ["/bin/sh", "-c", command], timeout_seconds=timeout_seconds
362
+ invocation.args, env=invocation.env, timeout_seconds=timeout_seconds
360
363
  )
361
364
  ok = result.exit_code == 0
362
365
  content = result.stdout or result.stderr
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ShellInvocation:
11
+ args: list[str]
12
+ env: dict[str, str]
13
+ shell: str
14
+
15
+
16
+ def executable_path(path: Path) -> str | None:
17
+ if path.is_file() and os.access(path, os.X_OK):
18
+ return str(path.resolve(strict=False))
19
+ return None
20
+
21
+
22
+ def executable_command_path(command: str) -> str | None:
23
+ resolved = shutil.which(command)
24
+ if resolved is None:
25
+ return None
26
+ return executable_path(Path(resolved))
27
+
28
+
29
+ def shell_path(raw_shell: str) -> str | None:
30
+ raw_shell = raw_shell.strip()
31
+ if not raw_shell:
32
+ return None
33
+ expanded = Path(raw_shell).expanduser()
34
+ if expanded.is_absolute():
35
+ return executable_path(expanded)
36
+ return executable_command_path(raw_shell)
37
+
38
+
39
+ def user_default_shell() -> str | None:
40
+ try:
41
+ import pwd
42
+ except ImportError:
43
+ return None
44
+
45
+ try:
46
+ shell = pwd.getpwuid(os.getuid()).pw_shell
47
+ except (AttributeError, KeyError, OSError):
48
+ return None
49
+ return shell_path(shell)
50
+
51
+
52
+ def environment_shell() -> str | None:
53
+ return shell_path(os.environ.get("SHELL", ""))
54
+
55
+
56
+ FALLBACK_SHELL_PATHS = {
57
+ "bash": [Path("/bin/bash"), Path("/usr/bin/bash")],
58
+ "sh": [Path("/bin/sh"), Path("/usr/bin/sh")],
59
+ }
60
+
61
+
62
+ def fallback_shell(command: str) -> str | None:
63
+ shell = executable_command_path(command)
64
+ if shell is not None:
65
+ return shell
66
+ for fallback in FALLBACK_SHELL_PATHS.get(command, []):
67
+ shell = executable_path(fallback)
68
+ if shell is not None:
69
+ return shell
70
+ return None
71
+
72
+
73
+ def default_shell() -> str:
74
+ for shell in [user_default_shell(), environment_shell()]:
75
+ if shell is not None:
76
+ return shell
77
+ for command in ["bash", "sh"]:
78
+ shell = fallback_shell(command)
79
+ if shell is not None:
80
+ return shell
81
+ return "sh"
82
+
83
+
84
+ def shell_invocation(command: str) -> ShellInvocation:
85
+ shell = default_shell()
86
+ return ShellInvocation(
87
+ args=[shell, "-c", command],
88
+ env={"SHELL": shell},
89
+ shell=shell,
90
+ )
91
+
92
+
93
+ def shell_invocation_description() -> str:
94
+ return f"{default_shell()} -c"