ptn 0.3.2__py3-none-any.whl → 0.4.6__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.
- porterminal/__init__.py +13 -5
- porterminal/_version.py +2 -2
- porterminal/application/services/management_service.py +28 -52
- porterminal/application/services/session_service.py +3 -11
- porterminal/application/services/terminal_service.py +97 -56
- porterminal/cli/args.py +84 -35
- porterminal/cli/display.py +18 -16
- porterminal/cli/script_discovery.py +266 -0
- porterminal/composition.py +2 -7
- porterminal/config.py +4 -2
- porterminal/domain/__init__.py +0 -9
- porterminal/domain/entities/output_buffer.py +56 -1
- porterminal/domain/entities/tab.py +11 -10
- porterminal/domain/services/__init__.py +0 -2
- porterminal/domain/values/__init__.py +0 -4
- porterminal/domain/values/environment_rules.py +3 -0
- porterminal/domain/values/rate_limit_config.py +3 -3
- porterminal/infrastructure/cloudflared.py +13 -11
- porterminal/infrastructure/config/shell_detector.py +113 -24
- porterminal/infrastructure/repositories/in_memory_session.py +1 -4
- porterminal/infrastructure/repositories/in_memory_tab.py +2 -10
- porterminal/pty/env.py +16 -78
- porterminal/pty/manager.py +6 -4
- porterminal/static/assets/app-DlWNJWFE.js +87 -0
- porterminal/static/assets/app-xPAM7YhQ.css +1 -0
- porterminal/static/index.html +2 -2
- porterminal/updater.py +13 -5
- {ptn-0.3.2.dist-info → ptn-0.4.6.dist-info}/METADATA +54 -16
- {ptn-0.3.2.dist-info → ptn-0.4.6.dist-info}/RECORD +32 -37
- porterminal/static/assets/app-BkHv5qu0.css +0 -32
- porterminal/static/assets/app-CaIGfw7i.js +0 -72
- porterminal/static/assets/app-D9ELFbEO.js +0 -72
- porterminal/static/assets/app-DF3nl_io.js +0 -72
- porterminal/static/assets/app-DQePboVd.css +0 -32
- porterminal/static/assets/app-DoBiVkTD.js +0 -72
- porterminal/static/assets/app-azbHOsRw.css +0 -32
- porterminal/static/assets/app-nMNFwMa6.css +0 -32
- {ptn-0.3.2.dist-info → ptn-0.4.6.dist-info}/WHEEL +0 -0
- {ptn-0.3.2.dist-info → ptn-0.4.6.dist-info}/entry_points.txt +0 -0
- {ptn-0.3.2.dist-info → ptn-0.4.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,6 +17,13 @@ console = Console()
|
|
|
17
17
|
class CloudflaredInstaller:
|
|
18
18
|
"""Platform-specific cloudflared installer."""
|
|
19
19
|
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _add_to_path(path: str | Path) -> None:
|
|
22
|
+
"""Add directory to PATH for current process."""
|
|
23
|
+
path_str = str(path)
|
|
24
|
+
os.environ["PATH"] = path_str + os.pathsep + os.environ.get("PATH", "")
|
|
25
|
+
console.print(f"[dim]Added to PATH: {path_str}[/dim]")
|
|
26
|
+
|
|
20
27
|
@staticmethod
|
|
21
28
|
def is_installed() -> bool:
|
|
22
29
|
"""Check if cloudflared is installed."""
|
|
@@ -91,8 +98,7 @@ class CloudflaredInstaller:
|
|
|
91
98
|
# Try to find and add to PATH for current session
|
|
92
99
|
install_path = CloudflaredInstaller._find_cloudflared_windows()
|
|
93
100
|
if install_path:
|
|
94
|
-
|
|
95
|
-
console.print(f"[dim]Added to PATH: {install_path}[/dim]")
|
|
101
|
+
CloudflaredInstaller._add_to_path(install_path)
|
|
96
102
|
# Return True regardless - winget succeeded, may just need shell restart
|
|
97
103
|
return True
|
|
98
104
|
except (subprocess.TimeoutExpired, OSError) as e:
|
|
@@ -119,7 +125,7 @@ class CloudflaredInstaller:
|
|
|
119
125
|
|
|
120
126
|
exe_path = install_dir / "cloudflared.exe"
|
|
121
127
|
if exe_path.exists():
|
|
122
|
-
|
|
128
|
+
CloudflaredInstaller._add_to_path(install_dir)
|
|
123
129
|
console.print(f"[green]✓ Installed to {install_dir}[/green]")
|
|
124
130
|
return True
|
|
125
131
|
|
|
@@ -203,10 +209,7 @@ class CloudflaredInstaller:
|
|
|
203
209
|
# Try to find and add to PATH
|
|
204
210
|
install_path = CloudflaredInstaller._find_cloudflared_unix()
|
|
205
211
|
if install_path:
|
|
206
|
-
|
|
207
|
-
install_path + os.pathsep + os.environ.get("PATH", "")
|
|
208
|
-
)
|
|
209
|
-
console.print(f"[dim]Added to PATH: {install_path}[/dim]")
|
|
212
|
+
CloudflaredInstaller._add_to_path(install_path)
|
|
210
213
|
# Return True regardless - package manager succeeded
|
|
211
214
|
return True
|
|
212
215
|
except (subprocess.TimeoutExpired, OSError) as e:
|
|
@@ -226,7 +229,7 @@ class CloudflaredInstaller:
|
|
|
226
229
|
os.chmod(bin_path, 0o755)
|
|
227
230
|
|
|
228
231
|
# Add to PATH for this session
|
|
229
|
-
|
|
232
|
+
CloudflaredInstaller._add_to_path(install_dir)
|
|
230
233
|
console.print(f"[green]✓ Installed to {bin_path}[/green]")
|
|
231
234
|
return True
|
|
232
235
|
|
|
@@ -257,8 +260,7 @@ class CloudflaredInstaller:
|
|
|
257
260
|
# Try to find and add to PATH
|
|
258
261
|
install_path = CloudflaredInstaller._find_cloudflared_unix()
|
|
259
262
|
if install_path:
|
|
260
|
-
|
|
261
|
-
console.print(f"[dim]Added to PATH: {install_path}[/dim]")
|
|
263
|
+
CloudflaredInstaller._add_to_path(install_path)
|
|
262
264
|
# Return True regardless - Homebrew succeeded
|
|
263
265
|
return True
|
|
264
266
|
except (subprocess.TimeoutExpired, OSError) as e:
|
|
@@ -289,7 +291,7 @@ class CloudflaredInstaller:
|
|
|
289
291
|
bin_path = install_dir / "cloudflared"
|
|
290
292
|
if bin_path.exists():
|
|
291
293
|
os.chmod(bin_path, 0o755)
|
|
292
|
-
|
|
294
|
+
CloudflaredInstaller._add_to_path(install_dir)
|
|
293
295
|
console.print(f"[green]✓ Installed to {bin_path}[/green]")
|
|
294
296
|
return True
|
|
295
297
|
|
|
@@ -21,6 +21,9 @@ class ShellDetector:
|
|
|
21
21
|
def detect_shells(self) -> list[ShellCommand]:
|
|
22
22
|
"""Auto-detect available shells.
|
|
23
23
|
|
|
24
|
+
Detects platform-specific shells and includes the user's $SHELL
|
|
25
|
+
if it's not already in the list (supports any shell).
|
|
26
|
+
|
|
24
27
|
Returns:
|
|
25
28
|
List of detected shell configurations.
|
|
26
29
|
"""
|
|
@@ -39,6 +42,12 @@ class ShellDetector:
|
|
|
39
42
|
)
|
|
40
43
|
)
|
|
41
44
|
|
|
45
|
+
# Include user's $SHELL if not already detected (supports unknown shells)
|
|
46
|
+
user_shell = self._create_shell_from_env()
|
|
47
|
+
if user_shell and not any(s.id == user_shell.id for s in shells):
|
|
48
|
+
# Insert at beginning so user's preferred shell is first
|
|
49
|
+
shells.insert(0, user_shell)
|
|
50
|
+
|
|
42
51
|
return shells
|
|
43
52
|
|
|
44
53
|
def get_default_shell_id(self) -> str:
|
|
@@ -52,11 +61,19 @@ class ShellDetector:
|
|
|
52
61
|
def _get_platform_candidates(self) -> list[tuple[str, str, str, list[str]]]:
|
|
53
62
|
"""Get shell candidates for current platform.
|
|
54
63
|
|
|
64
|
+
On Windows, discovers shells from:
|
|
65
|
+
- Windows Terminal profiles (includes WSL distros)
|
|
66
|
+
- Hardcoded common shells (PowerShell, CMD, Git Bash)
|
|
67
|
+
- Visual Studio Developer shells
|
|
68
|
+
|
|
69
|
+
Note: WSL distros are detected from Windows Terminal profiles only.
|
|
70
|
+
We don't probe inside WSL for individual shells to avoid crossing
|
|
71
|
+
environment boundaries.
|
|
72
|
+
|
|
55
73
|
Returns:
|
|
56
74
|
List of (name, id, command, args) tuples.
|
|
57
75
|
"""
|
|
58
76
|
if sys.platform == "win32":
|
|
59
|
-
# Get Windows Terminal profiles first, then hardcoded defaults
|
|
60
77
|
wt_profiles = self._get_windows_terminal_profiles()
|
|
61
78
|
hardcoded = [
|
|
62
79
|
("PS 7", "pwsh", "pwsh.exe", ["-NoLogo"]),
|
|
@@ -65,15 +82,18 @@ class ShellDetector:
|
|
|
65
82
|
("WSL", "wsl", "wsl.exe", []),
|
|
66
83
|
("Git Bash", "gitbash", r"C:\Program Files\Git\bin\bash.exe", ["--login"]),
|
|
67
84
|
]
|
|
68
|
-
# Merge WT profiles with hardcoded (dedupe by command)
|
|
69
85
|
merged = self._merge_candidates(wt_profiles, hardcoded)
|
|
70
|
-
# Add VS dev shells (no deduplication - they're unique due to args)
|
|
71
86
|
vs_shells = self._get_visual_studio_shells()
|
|
72
87
|
return merged + vs_shells
|
|
73
88
|
return [
|
|
74
89
|
("Bash", "bash", "bash", ["--login"]),
|
|
75
90
|
("Zsh", "zsh", "zsh", ["--login"]),
|
|
76
91
|
("Fish", "fish", "fish", []),
|
|
92
|
+
("Nu", "nu", "nu", []),
|
|
93
|
+
("Ion", "ion", "ion", []),
|
|
94
|
+
("Dash", "dash", "dash", []),
|
|
95
|
+
("Ksh", "ksh", "ksh", ["--login"]),
|
|
96
|
+
("Tcsh", "tcsh", "tcsh", []),
|
|
77
97
|
("Sh", "sh", "sh", []),
|
|
78
98
|
]
|
|
79
99
|
|
|
@@ -109,8 +129,20 @@ class ShellDetector:
|
|
|
109
129
|
for profile in profile_list:
|
|
110
130
|
name = profile.get("name", "")
|
|
111
131
|
commandline = profile.get("commandline", "")
|
|
132
|
+
source = profile.get("source", "")
|
|
112
133
|
|
|
113
|
-
if not name
|
|
134
|
+
if not name:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Handle WSL distro profiles (they use source instead of commandline)
|
|
138
|
+
if source == "Windows.Terminal.Wsl" and not commandline:
|
|
139
|
+
# Use wsl.exe -d <distro> to launch the specific distro
|
|
140
|
+
shell_id = self._slugify(name)
|
|
141
|
+
short_name = self._abbreviate_name(name)
|
|
142
|
+
profiles.append((short_name, shell_id, "wsl.exe", ["-d", name]))
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if not commandline:
|
|
114
146
|
continue
|
|
115
147
|
|
|
116
148
|
# Parse commandline into command and args
|
|
@@ -149,25 +181,20 @@ class ShellDetector:
|
|
|
149
181
|
|
|
150
182
|
try:
|
|
151
183
|
result = subprocess.run(
|
|
152
|
-
[
|
|
153
|
-
str(vswhere),
|
|
154
|
-
"-all",
|
|
155
|
-
"-prerelease",
|
|
156
|
-
"-property",
|
|
157
|
-
"installationPath",
|
|
158
|
-
],
|
|
184
|
+
[str(vswhere), "-all", "-prerelease", "-format", "json"],
|
|
159
185
|
capture_output=True,
|
|
160
186
|
text=True,
|
|
161
187
|
timeout=5,
|
|
162
188
|
)
|
|
163
|
-
|
|
164
|
-
except (subprocess.TimeoutExpired, OSError) as e:
|
|
189
|
+
vs_installs = json.loads(result.stdout) if result.stdout.strip() else []
|
|
190
|
+
except (subprocess.TimeoutExpired, OSError, json.JSONDecodeError) as e:
|
|
165
191
|
logger.warning("Failed to run vswhere: %s", e)
|
|
166
192
|
return []
|
|
167
193
|
|
|
168
194
|
shells = []
|
|
169
|
-
for
|
|
170
|
-
vs_path = Path(
|
|
195
|
+
for vs_info in vs_installs:
|
|
196
|
+
vs_path = Path(vs_info.get("installationPath", ""))
|
|
197
|
+
instance_id = vs_info.get("instanceId", "")
|
|
171
198
|
# Extract VS version and edition from path
|
|
172
199
|
# e.g., "C:\Program Files\Microsoft Visual Studio\2022\Community"
|
|
173
200
|
edition = vs_path.name # Community, Professional, Enterprise
|
|
@@ -188,18 +215,32 @@ class ShellDetector:
|
|
|
188
215
|
)
|
|
189
216
|
)
|
|
190
217
|
|
|
191
|
-
# Developer PowerShell
|
|
192
|
-
|
|
193
|
-
|
|
218
|
+
# Developer PowerShell - find DevShell.dll (location varies by VS version)
|
|
219
|
+
devshell_dll = None
|
|
220
|
+
for dll_path in [
|
|
221
|
+
vs_path / "Common7" / "Tools" / "Microsoft.VisualStudio.DevShell.dll",
|
|
222
|
+
vs_path
|
|
223
|
+
/ "Common7"
|
|
224
|
+
/ "Tools"
|
|
225
|
+
/ "vsdevshell"
|
|
226
|
+
/ "Microsoft.VisualStudio.DevShell.dll",
|
|
227
|
+
]:
|
|
228
|
+
if dll_path.exists():
|
|
229
|
+
devshell_dll = dll_path
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
if devshell_dll and instance_id:
|
|
194
233
|
name = f"Dev PS {year}"
|
|
195
234
|
shell_id = f"devps-{year}-{edition.lower()}"
|
|
196
|
-
#
|
|
235
|
+
# Use forward slashes to avoid backslash escape issues (PowerShell accepts both)
|
|
236
|
+
dll_str = str(devshell_dll).replace("\\", "/")
|
|
237
|
+
cmd = f"Import-Module '{dll_str}'; Enter-VsDevShell {instance_id} -SkipAutomaticLocation"
|
|
197
238
|
shells.append(
|
|
198
239
|
(
|
|
199
240
|
name,
|
|
200
241
|
shell_id,
|
|
201
242
|
"powershell.exe",
|
|
202
|
-
["-NoExit", "-Command",
|
|
243
|
+
["-NoExit", "-Command", cmd],
|
|
203
244
|
)
|
|
204
245
|
)
|
|
205
246
|
|
|
@@ -405,16 +446,23 @@ class ShellDetector:
|
|
|
405
446
|
"""Get shell ID from user's $SHELL environment variable.
|
|
406
447
|
|
|
407
448
|
Returns:
|
|
408
|
-
Shell ID if $SHELL is set and
|
|
449
|
+
Shell ID if $SHELL is set and valid, None otherwise.
|
|
450
|
+
For unknown shells, returns the executable name as the ID.
|
|
409
451
|
"""
|
|
410
452
|
shell_path = os.environ.get("SHELL", "")
|
|
411
453
|
if not shell_path:
|
|
412
454
|
return None
|
|
413
455
|
|
|
456
|
+
path = Path(shell_path)
|
|
457
|
+
|
|
458
|
+
# Validate shell exists
|
|
459
|
+
if not path.exists() and not shutil.which(shell_path):
|
|
460
|
+
return None
|
|
461
|
+
|
|
414
462
|
# Extract shell name from path (e.g., /usr/bin/fish -> fish)
|
|
415
|
-
shell_name =
|
|
463
|
+
shell_name = path.name.lower()
|
|
416
464
|
|
|
417
|
-
# Map common shell names to IDs
|
|
465
|
+
# Map common shell names to canonical IDs (for consistency)
|
|
418
466
|
shell_map = {
|
|
419
467
|
"bash": "bash",
|
|
420
468
|
"zsh": "zsh",
|
|
@@ -422,4 +470,45 @@ class ShellDetector:
|
|
|
422
470
|
"sh": "sh",
|
|
423
471
|
}
|
|
424
472
|
|
|
425
|
-
|
|
473
|
+
# Return known ID or use executable name for unknown shells
|
|
474
|
+
return shell_map.get(shell_name, shell_name)
|
|
475
|
+
|
|
476
|
+
def _create_shell_from_env(self) -> ShellCommand | None:
|
|
477
|
+
"""Create a ShellCommand from user's $SHELL environment variable.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
ShellCommand if $SHELL is set and valid, None otherwise.
|
|
481
|
+
"""
|
|
482
|
+
shell_path = os.environ.get("SHELL", "")
|
|
483
|
+
if not shell_path:
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
path = Path(shell_path)
|
|
487
|
+
|
|
488
|
+
# Validate shell exists
|
|
489
|
+
if not path.exists() and not shutil.which(shell_path):
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
shell_name = path.name.lower()
|
|
493
|
+
|
|
494
|
+
# Known shells with their display names and args
|
|
495
|
+
known_shells = {
|
|
496
|
+
"bash": ("Bash", ["--login"]),
|
|
497
|
+
"zsh": ("Zsh", ["--login"]),
|
|
498
|
+
"fish": ("Fish", []),
|
|
499
|
+
"sh": ("Sh", []),
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if shell_name in known_shells:
|
|
503
|
+
display_name, args = known_shells[shell_name]
|
|
504
|
+
else:
|
|
505
|
+
# Unknown shell - use capitalized name, no special args
|
|
506
|
+
display_name = shell_name.capitalize()
|
|
507
|
+
args = []
|
|
508
|
+
|
|
509
|
+
return ShellCommand(
|
|
510
|
+
id=shell_name,
|
|
511
|
+
name=display_name,
|
|
512
|
+
command=shell_path,
|
|
513
|
+
args=tuple(args),
|
|
514
|
+
)
|
|
@@ -38,10 +38,7 @@ class InMemorySessionRepository(SessionRepository[PTYHandle]):
|
|
|
38
38
|
user_id = str(session.user_id)
|
|
39
39
|
|
|
40
40
|
self._sessions[session_id] = session
|
|
41
|
-
|
|
42
|
-
if user_id not in self._user_sessions:
|
|
43
|
-
self._user_sessions[user_id] = set()
|
|
44
|
-
self._user_sessions[user_id].add(session_id)
|
|
41
|
+
self._user_sessions.setdefault(user_id, set()).add(session_id)
|
|
45
42
|
|
|
46
43
|
def remove(self, session_id: SessionId) -> Session[PTYHandle] | None:
|
|
47
44
|
"""Remove and return a session."""
|
|
@@ -44,16 +44,8 @@ class InMemoryTabRepository(TabRepository):
|
|
|
44
44
|
session_id = str(tab.session_id)
|
|
45
45
|
|
|
46
46
|
self._tabs[tab_id] = tab
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if user_id not in self._user_tabs:
|
|
50
|
-
self._user_tabs[user_id] = set()
|
|
51
|
-
self._user_tabs[user_id].add(tab_id)
|
|
52
|
-
|
|
53
|
-
# Index by session
|
|
54
|
-
if session_id not in self._session_tabs:
|
|
55
|
-
self._session_tabs[session_id] = set()
|
|
56
|
-
self._session_tabs[session_id].add(tab_id)
|
|
47
|
+
self._user_tabs.setdefault(user_id, set()).add(tab_id)
|
|
48
|
+
self._session_tabs.setdefault(session_id, set()).add(tab_id)
|
|
57
49
|
|
|
58
50
|
def update(self, tab: Tab) -> None:
|
|
59
51
|
"""Update an existing tab (name, last_accessed)."""
|
porterminal/pty/env.py
CHANGED
|
@@ -1,94 +1,32 @@
|
|
|
1
|
-
"""Environment variable sanitization for PTY processes.
|
|
1
|
+
"""Environment variable sanitization for PTY processes.
|
|
2
|
+
|
|
3
|
+
This module re-exports the environment rules from the domain layer
|
|
4
|
+
and provides the build_safe_environment() function for PTY spawning.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
import os
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{
|
|
8
|
-
# System paths
|
|
9
|
-
"PATH",
|
|
10
|
-
"PATHEXT",
|
|
11
|
-
"SYSTEMROOT",
|
|
12
|
-
"WINDIR",
|
|
13
|
-
"TEMP",
|
|
14
|
-
"TMP",
|
|
15
|
-
"COMSPEC",
|
|
16
|
-
# User directories
|
|
17
|
-
"HOME",
|
|
18
|
-
"USERPROFILE",
|
|
19
|
-
"HOMEDRIVE",
|
|
20
|
-
"HOMEPATH",
|
|
21
|
-
"LOCALAPPDATA",
|
|
22
|
-
"APPDATA",
|
|
23
|
-
"PROGRAMFILES",
|
|
24
|
-
"PROGRAMFILES(X86)",
|
|
25
|
-
"COMMONPROGRAMFILES",
|
|
26
|
-
# System info
|
|
27
|
-
"COMPUTERNAME",
|
|
28
|
-
"USERNAME",
|
|
29
|
-
"USERDOMAIN",
|
|
30
|
-
"OS",
|
|
31
|
-
"PROCESSOR_ARCHITECTURE",
|
|
32
|
-
"NUMBER_OF_PROCESSORS",
|
|
33
|
-
# Terminal
|
|
34
|
-
"TERM",
|
|
35
|
-
# Locale settings for proper text rendering
|
|
36
|
-
"LANG",
|
|
37
|
-
"LC_ALL",
|
|
38
|
-
"LC_CTYPE",
|
|
39
|
-
}
|
|
9
|
+
from porterminal.domain.values.environment_rules import (
|
|
10
|
+
DEFAULT_BLOCKED_VARS as BLOCKED_ENV_VARS,
|
|
40
11
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
BLOCKED_ENV_VARS: frozenset[str] = frozenset(
|
|
44
|
-
{
|
|
45
|
-
# AWS
|
|
46
|
-
"AWS_ACCESS_KEY_ID",
|
|
47
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
48
|
-
"AWS_SESSION_TOKEN",
|
|
49
|
-
# Azure
|
|
50
|
-
"AZURE_CLIENT_SECRET",
|
|
51
|
-
"AZURE_CLIENT_ID",
|
|
52
|
-
# Git/GitHub/GitLab
|
|
53
|
-
"GH_TOKEN",
|
|
54
|
-
"GITHUB_TOKEN",
|
|
55
|
-
"GITLAB_TOKEN",
|
|
56
|
-
# Package managers
|
|
57
|
-
"NPM_TOKEN",
|
|
58
|
-
# AI APIs
|
|
59
|
-
"ANTHROPIC_API_KEY",
|
|
60
|
-
"OPENAI_API_KEY",
|
|
61
|
-
"GOOGLE_API_KEY",
|
|
62
|
-
# Payment
|
|
63
|
-
"STRIPE_SECRET_KEY",
|
|
64
|
-
# Database
|
|
65
|
-
"DATABASE_URL",
|
|
66
|
-
"DB_PASSWORD",
|
|
67
|
-
# Generic secrets
|
|
68
|
-
"SECRET_KEY",
|
|
69
|
-
"API_KEY",
|
|
70
|
-
"API_SECRET",
|
|
71
|
-
"PRIVATE_KEY",
|
|
72
|
-
}
|
|
12
|
+
from porterminal.domain.values.environment_rules import (
|
|
13
|
+
DEFAULT_SAFE_VARS as SAFE_ENV_VARS,
|
|
73
14
|
)
|
|
74
15
|
|
|
16
|
+
# Re-export for backward compatibility
|
|
17
|
+
__all__ = ["SAFE_ENV_VARS", "BLOCKED_ENV_VARS", "build_safe_environment"]
|
|
18
|
+
|
|
75
19
|
|
|
76
20
|
def build_safe_environment() -> dict[str, str]:
|
|
77
21
|
"""Build a sanitized environment for the PTY.
|
|
78
22
|
|
|
23
|
+
Uses allowlist approach - only SAFE_ENV_VARS are copied,
|
|
24
|
+
so BLOCKED_ENV_VARS can never be included.
|
|
25
|
+
|
|
79
26
|
Returns:
|
|
80
27
|
Dictionary of safe environment variables.
|
|
81
28
|
"""
|
|
82
|
-
safe_env:
|
|
83
|
-
|
|
84
|
-
# Copy only allowed variables
|
|
85
|
-
for var in SAFE_ENV_VARS:
|
|
86
|
-
if var in os.environ:
|
|
87
|
-
safe_env[var] = os.environ[var]
|
|
88
|
-
|
|
89
|
-
# Ensure blocked vars are not included (defense in depth)
|
|
90
|
-
for var in BLOCKED_ENV_VARS:
|
|
91
|
-
safe_env.pop(var, None)
|
|
29
|
+
safe_env = {var: os.environ[var] for var in SAFE_ENV_VARS if var in os.environ}
|
|
92
30
|
|
|
93
31
|
# Set custom variables for audit trail
|
|
94
32
|
safe_env["TERM"] = "xterm-256color"
|
porterminal/pty/manager.py
CHANGED
|
@@ -5,6 +5,8 @@ import shutil
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from porterminal.domain.values import MAX_COLS, MAX_ROWS, MIN_COLS, MIN_ROWS
|
|
9
|
+
|
|
8
10
|
from .env import build_safe_environment
|
|
9
11
|
from .protocol import PTYBackend
|
|
10
12
|
|
|
@@ -43,8 +45,8 @@ class SecurePTYManager:
|
|
|
43
45
|
"""
|
|
44
46
|
self._backend = backend
|
|
45
47
|
self.shell_config = shell_config
|
|
46
|
-
self.cols = max(
|
|
47
|
-
self.rows = max(
|
|
48
|
+
self.cols = max(MIN_COLS, min(cols, MAX_COLS))
|
|
49
|
+
self.rows = max(MIN_ROWS, min(rows, MAX_ROWS))
|
|
48
50
|
self.cwd = cwd
|
|
49
51
|
self._closed = False
|
|
50
52
|
|
|
@@ -133,8 +135,8 @@ class SecurePTYManager:
|
|
|
133
135
|
if self._closed:
|
|
134
136
|
return
|
|
135
137
|
|
|
136
|
-
cols = max(
|
|
137
|
-
rows = max(
|
|
138
|
+
cols = max(MIN_COLS, min(cols, MAX_COLS))
|
|
139
|
+
rows = max(MIN_ROWS, min(rows, MAX_ROWS))
|
|
138
140
|
|
|
139
141
|
try:
|
|
140
142
|
self._backend.resize(rows, cols)
|