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.
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/agent.py +1 -0
- package/backend/src/flowent/approval.py +6 -4
- package/backend/src/flowent/context.py +2 -0
- package/backend/src/flowent/llm.py +9 -4
- package/backend/src/flowent/main.py +447 -81
- package/backend/src/flowent/permissions.py +5 -2
- package/backend/src/flowent/shell.py +94 -0
- package/backend/src/flowent/static/assets/index-D7t9qNrC.js +82 -0
- package/backend/src/flowent/static/assets/index-DufpDl8x.css +2 -0
- package/backend/src/flowent/static/index.html +2 -2
- package/backend/src/flowent/storage.py +16 -4
- package/backend/src/flowent/tools.py +5 -2
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/index-D7t9qNrC.js +82 -0
- package/dist/frontend/assets/index-DufpDl8x.css +2 -0
- package/dist/frontend/index.html +2 -2
- package/package.json +1 -1
- package/backend/src/flowent/static/assets/index-CRSV2xu1.css +0 -2
- package/backend/src/flowent/static/assets/index-DUYj6rgD.js +0 -82
- package/dist/frontend/assets/index-CRSV2xu1.css +0 -2
- package/dist/frontend/assets/index-DUYj6rgD.js +0 -82
|
@@ -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(
|
|
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
|
-
|
|
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"
|