indent 0.1.13__py3-none-any.whl → 0.1.28__py3-none-any.whl
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.
- exponent/__init__.py +2 -2
- exponent/cli.py +0 -2
- exponent/commands/cloud_commands.py +2 -87
- exponent/commands/common.py +25 -40
- exponent/commands/config_commands.py +0 -87
- exponent/commands/run_commands.py +5 -2
- exponent/core/config.py +1 -1
- exponent/core/container_build/__init__.py +0 -0
- exponent/core/container_build/types.py +25 -0
- exponent/core/graphql/mutations.py +2 -31
- exponent/core/graphql/queries.py +0 -3
- exponent/core/remote_execution/cli_rpc_types.py +201 -5
- exponent/core/remote_execution/client.py +355 -92
- exponent/core/remote_execution/code_execution.py +26 -7
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/languages/shell_streaming.py +11 -6
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/system_context.py +2 -0
- exponent/core/remote_execution/terminal_session.py +517 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +228 -18
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +9 -1
- exponent/core/remote_execution/types.py +71 -19
- exponent/utils/version.py +8 -7
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/METADATA +5 -2
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/RECORD +29 -24
- exponent/commands/workflow_commands.py +0 -111
- exponent/core/graphql/github_config_queries.py +0 -56
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/WHEEL +0 -0
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/entry_points.txt +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator, Callable
|
|
2
2
|
|
|
3
|
+
from exponent.core.remote_execution.cli_rpc_types import (
|
|
4
|
+
StreamingCodeExecutionRequest,
|
|
5
|
+
StreamingCodeExecutionResponse,
|
|
6
|
+
StreamingCodeExecutionResponseChunk,
|
|
7
|
+
)
|
|
3
8
|
from exponent.core.remote_execution.languages.python_execution import (
|
|
4
9
|
execute_python_streaming,
|
|
5
10
|
)
|
|
@@ -8,13 +13,9 @@ from exponent.core.remote_execution.languages.shell_streaming import (
|
|
|
8
13
|
)
|
|
9
14
|
from exponent.core.remote_execution.languages.types import StreamedOutputPiece
|
|
10
15
|
from exponent.core.remote_execution.session import RemoteExecutionClientSession
|
|
11
|
-
from exponent.core.remote_execution.types import (
|
|
12
|
-
StreamingCodeExecutionRequest,
|
|
13
|
-
StreamingCodeExecutionResponse,
|
|
14
|
-
StreamingCodeExecutionResponseChunk,
|
|
15
|
-
)
|
|
16
16
|
|
|
17
17
|
EMPTY_OUTPUT_STRING = "(No output)"
|
|
18
|
+
MAX_OUTPUT_LENGTH = 50000 # Maximum characters to keep in final output
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
async def execute_code_streaming(
|
|
@@ -34,9 +35,18 @@ async def execute_code_streaming(
|
|
|
34
35
|
content=output.content, correlation_id=request.correlation_id
|
|
35
36
|
)
|
|
36
37
|
else:
|
|
38
|
+
final_output = output.output or EMPTY_OUTPUT_STRING
|
|
39
|
+
truncated = len(final_output) > MAX_OUTPUT_LENGTH
|
|
40
|
+
if truncated:
|
|
41
|
+
final_output = final_output[-MAX_OUTPUT_LENGTH:]
|
|
42
|
+
final_output = (
|
|
43
|
+
f"[Truncated to last {MAX_OUTPUT_LENGTH} characters]\n\n"
|
|
44
|
+
+ final_output
|
|
45
|
+
)
|
|
37
46
|
yield StreamingCodeExecutionResponse(
|
|
38
47
|
correlation_id=request.correlation_id,
|
|
39
|
-
content=
|
|
48
|
+
content=final_output,
|
|
49
|
+
truncated=truncated,
|
|
40
50
|
halted=output.halted,
|
|
41
51
|
)
|
|
42
52
|
|
|
@@ -49,9 +59,18 @@ async def execute_code_streaming(
|
|
|
49
59
|
content=shell_output.content, correlation_id=request.correlation_id
|
|
50
60
|
)
|
|
51
61
|
else:
|
|
62
|
+
final_output = shell_output.output or EMPTY_OUTPUT_STRING
|
|
63
|
+
truncated = len(final_output) > MAX_OUTPUT_LENGTH
|
|
64
|
+
if truncated:
|
|
65
|
+
final_output = final_output[-MAX_OUTPUT_LENGTH:]
|
|
66
|
+
final_output = (
|
|
67
|
+
f"[Truncated to last {MAX_OUTPUT_LENGTH} characters]\n\n"
|
|
68
|
+
+ final_output
|
|
69
|
+
)
|
|
52
70
|
yield StreamingCodeExecutionResponse(
|
|
53
71
|
correlation_id=request.correlation_id,
|
|
54
|
-
content=
|
|
72
|
+
content=final_output,
|
|
73
|
+
truncated=truncated,
|
|
55
74
|
halted=shell_output.halted,
|
|
56
75
|
exit_code=shell_output.exit_code,
|
|
57
76
|
cancelled_for_timeout=shell_output.cancelled_for_timeout,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_default_env() -> dict[str, str]:
|
|
5
|
+
"""
|
|
6
|
+
Returns default environment variables for CLI-spawned processes.
|
|
7
|
+
These are merged with the parent process environment.
|
|
8
|
+
"""
|
|
9
|
+
return {
|
|
10
|
+
"GIT_EDITOR": "true",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_process_env(env_overrides: dict[str, str] | None = None) -> dict[str, str]:
|
|
15
|
+
"""
|
|
16
|
+
Returns the complete environment for spawned processes.
|
|
17
|
+
Merges parent environment with default variables, then applies overrides.
|
|
18
|
+
|
|
19
|
+
Priority order (lowest to highest):
|
|
20
|
+
1. Parent process environment (os.environ)
|
|
21
|
+
2. Default environment variables (get_default_env())
|
|
22
|
+
3. Explicit overrides (env_overrides parameter)
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
env_overrides: Optional dict of environment variables that override defaults
|
|
26
|
+
"""
|
|
27
|
+
env = os.environ.copy()
|
|
28
|
+
env.update(get_default_env())
|
|
29
|
+
if env_overrides:
|
|
30
|
+
env.update(env_overrides)
|
|
31
|
+
return env
|
|
@@ -6,6 +6,7 @@ import signal
|
|
|
6
6
|
from collections.abc import AsyncGenerator, Callable
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from exponent.core.remote_execution.default_env import get_process_env
|
|
9
10
|
from exponent.core.remote_execution.languages.types import (
|
|
10
11
|
ShellExecutionResult,
|
|
11
12
|
StreamedOutputPiece,
|
|
@@ -49,10 +50,10 @@ async def read_stream(
|
|
|
49
50
|
|
|
50
51
|
while True:
|
|
51
52
|
try:
|
|
52
|
-
data = await stream.read(
|
|
53
|
+
data = await stream.read(50_000)
|
|
53
54
|
if not data:
|
|
54
55
|
break
|
|
55
|
-
chunk = data.decode(encoding=encoding)
|
|
56
|
+
chunk = data.decode(encoding=encoding, errors="replace")
|
|
56
57
|
output.append((fd, chunk))
|
|
57
58
|
yield StreamedOutputPiece(content=chunk)
|
|
58
59
|
except UnicodeDecodeError:
|
|
@@ -80,6 +81,7 @@ async def execute_shell_streaming( # noqa: PLR0915
|
|
|
80
81
|
working_directory: str,
|
|
81
82
|
timeout: int,
|
|
82
83
|
should_halt: Callable[[], bool] | None = None,
|
|
84
|
+
env: dict[str, str] | None = None,
|
|
83
85
|
) -> AsyncGenerator[StreamedOutputPiece | ShellExecutionResult, None]:
|
|
84
86
|
timeout_seconds = min(timeout, MAX_TIMEOUT)
|
|
85
87
|
|
|
@@ -91,6 +93,7 @@ async def execute_shell_streaming( # noqa: PLR0915
|
|
|
91
93
|
stdout=asyncio.subprocess.PIPE,
|
|
92
94
|
stderr=asyncio.subprocess.PIPE,
|
|
93
95
|
cwd=working_directory,
|
|
96
|
+
env=get_process_env(env),
|
|
94
97
|
)
|
|
95
98
|
else:
|
|
96
99
|
# Add rc file sourcing to the command
|
|
@@ -105,6 +108,7 @@ async def execute_shell_streaming( # noqa: PLR0915
|
|
|
105
108
|
stdout=asyncio.subprocess.PIPE,
|
|
106
109
|
stderr=asyncio.subprocess.PIPE,
|
|
107
110
|
cwd=working_directory,
|
|
111
|
+
env=get_process_env(env),
|
|
108
112
|
start_new_session=True if platform.system() != "Windows" else False,
|
|
109
113
|
)
|
|
110
114
|
|
|
@@ -149,7 +153,10 @@ async def execute_shell_streaming( # noqa: PLR0915
|
|
|
149
153
|
def on_timeout() -> None:
|
|
150
154
|
nonlocal timed_out
|
|
151
155
|
timed_out = True
|
|
152
|
-
|
|
156
|
+
try:
|
|
157
|
+
process.kill()
|
|
158
|
+
except ProcessLookupError:
|
|
159
|
+
pass
|
|
153
160
|
|
|
154
161
|
try:
|
|
155
162
|
halt_task = asyncio.create_task(monitor_halt()) if should_halt else None
|
|
@@ -175,10 +182,8 @@ async def execute_shell_streaming( # noqa: PLR0915
|
|
|
175
182
|
piece = await task
|
|
176
183
|
yield piece
|
|
177
184
|
|
|
178
|
-
if process.returncode is not None:
|
|
179
|
-
break
|
|
180
|
-
|
|
181
185
|
# Schedule next read from the same stream
|
|
186
|
+
# Don't check process.returncode here - we need to drain all buffered output
|
|
182
187
|
if task is stdout_task and not process.stdout.at_eof():
|
|
183
188
|
stdout_task = asyncio.create_task(stdout_gen.__anext__())
|
|
184
189
|
pending.add(stdout_task)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
from exponent.core.remote_execution.types import PortInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_port_usage() -> list[PortInfo] | None:
|
|
9
|
+
"""
|
|
10
|
+
Get information about all listening ports on the system.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
List of PortInfo objects containing process name, port, protocol, pid, and uptime.
|
|
14
|
+
Returns None if there's a permission error.
|
|
15
|
+
Returns empty list if no listening ports are found.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
connections = psutil.net_connections(kind="tcp")
|
|
19
|
+
except (psutil.AccessDenied, PermissionError):
|
|
20
|
+
# If we don't have permission to see connections, return None
|
|
21
|
+
return None
|
|
22
|
+
except Exception:
|
|
23
|
+
# For any other unexpected errors, return None
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
port_info_list: list[PortInfo] = []
|
|
27
|
+
current_time = time.time()
|
|
28
|
+
|
|
29
|
+
for conn in connections:
|
|
30
|
+
# Only include TCP ports in LISTEN state
|
|
31
|
+
if conn.status != "LISTEN":
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
# Skip if no local address (shouldn't happen for LISTEN, but be safe)
|
|
35
|
+
if not conn.laddr:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
port = conn.laddr.port
|
|
39
|
+
pid = conn.pid
|
|
40
|
+
|
|
41
|
+
# Try to get process information
|
|
42
|
+
process_name = "unknown"
|
|
43
|
+
uptime_seconds = None
|
|
44
|
+
|
|
45
|
+
if pid:
|
|
46
|
+
try:
|
|
47
|
+
process = psutil.Process(pid)
|
|
48
|
+
process_name = process.name()
|
|
49
|
+
|
|
50
|
+
# Calculate uptime
|
|
51
|
+
create_time = process.create_time()
|
|
52
|
+
uptime_seconds = current_time - create_time
|
|
53
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
54
|
+
# Process disappeared or we don't have permission
|
|
55
|
+
pass
|
|
56
|
+
except Exception:
|
|
57
|
+
# Any other unexpected error, just skip process info
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
port_info = PortInfo(
|
|
61
|
+
process_name=process_name,
|
|
62
|
+
port=port,
|
|
63
|
+
protocol="TCP",
|
|
64
|
+
pid=pid,
|
|
65
|
+
uptime_seconds=uptime_seconds,
|
|
66
|
+
)
|
|
67
|
+
port_info_list.append(port_info)
|
|
68
|
+
|
|
69
|
+
# Limit to 50 ports to avoid bloating the heartbeat payload
|
|
70
|
+
if len(port_info_list) >= 50:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
return port_info_list
|
|
@@ -4,6 +4,7 @@ import platform
|
|
|
4
4
|
|
|
5
5
|
from exponent.core.remote_execution.git import get_git_info
|
|
6
6
|
from exponent.core.remote_execution.languages import python_execution
|
|
7
|
+
from exponent.core.remote_execution.port_utils import get_port_usage
|
|
7
8
|
from exponent.core.remote_execution.types import (
|
|
8
9
|
SystemInfo,
|
|
9
10
|
)
|
|
@@ -17,6 +18,7 @@ async def get_system_info(working_directory: str) -> SystemInfo:
|
|
|
17
18
|
shell=_get_user_shell(),
|
|
18
19
|
git=await get_git_info(working_directory),
|
|
19
20
|
python_env=python_execution.get_python_env_info(),
|
|
21
|
+
port_usage=get_port_usage(),
|
|
20
22
|
)
|
|
21
23
|
|
|
22
24
|
|