PyVaultRCE 2.3.0__tar.gz → 2.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyVaultRCE
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
5
5
  Author: PyVault
6
6
  Keywords: remote execution code hosting vault rce session
@@ -25,7 +25,7 @@ Dynamic: summary
25
25
 
26
26
  # PyVaultRCE
27
27
 
28
- Remote Code Execution & Hosting client for **PyVault**.
28
+ Remote Code Hosting client for **PyVault**.
29
29
 
30
30
  Upload Python scripts to a PyVault server and execute them remotely — the source code **never leaves the server**. Clients only ever hold a 21-character hex Session ID.
31
31
 
@@ -47,9 +47,22 @@ sid = CodeManager.enc("my_script.py")
47
47
  # Upload from a paste service URL
48
48
  sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
49
49
 
50
- # Execute remotely (source stays on server)
50
+ # Execute in a short-lived isolated subprocess by default
51
51
  CodeManager.run(sid)
52
52
 
53
+ # Upload with a short share link and library metadata
54
+ sid = CodeManager.enc(
55
+ "my_script.py",
56
+ label="analytics demo",
57
+ libraries=["requests", "pandas"],
58
+ username="your_name",
59
+ description="A public demo post",
60
+ tags=["python", "demo"],
61
+ )
62
+
63
+ # Hide PyVault status messages while running
64
+ CodeManager.run(sid, show_terminal=False)
65
+
53
66
  # Check metadata without incrementing execution counter
54
67
  CodeManager.info(sid)
55
68
 
@@ -62,16 +75,29 @@ CodeManager.ping()
62
75
  Set `PYVAULT_URL` to point at your hosted PyVault server:
63
76
 
64
77
  ```bash
65
- export PYVAULT_URL=https://your-server.replit.app
78
+ export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
66
79
  ```
67
80
 
81
+ The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
82
+ For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
83
+ You can also control status output globally:
84
+
85
+ ```bash
86
+ export PYVAULT_TERMINAL=off
87
+ ```
88
+
89
+ Library names are metadata only. PyVault does not auto-install packages or
90
+ execute dependency installers; install the libraries in the Python environment
91
+ where `CodeManager.run()` executes.
92
+
68
93
  ## API Reference
69
94
 
70
95
  | Method | Description |
71
96
  |--------|-------------|
72
97
  | `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
98
+ | `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
73
99
  | `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
74
- | `CodeManager.run(session_id)` | Fetch and execute code by Session ID |
100
+ | `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
75
101
  | `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
76
102
  | `CodeManager.ping()` | Check server connectivity |
77
103
  | `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
@@ -102,6 +128,11 @@ export PYVAULT_URL=https://your-server.replit.app
102
128
  - Session IDs are 21-character cryptographically random hex strings
103
129
  - Source code is never written to disk on the client
104
130
  - Code is encrypted at rest with Fernet on the server
131
+ - Code runs in a short-lived subprocess by default with a clean environment,
132
+ temporary working directory, CPU/memory/file-size limits, and a hard timeout
133
+ - `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
134
+ it is not recommended for untrusted sessions
135
+ - Share links expose only session metadata and declared library names, never source
105
136
  - Admin edits require the admin access code; owner edits/deletes require the one-time owner token
106
137
  - Sessions never expire unless an expiry is explicitly selected
107
138
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyVaultRCE
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Remote Code Execution & Hosting client for PyVault — source never exposed
5
5
  Author: PyVault
6
6
  Keywords: remote execution code hosting vault rce session
@@ -25,7 +25,7 @@ Dynamic: summary
25
25
 
26
26
  # PyVaultRCE
27
27
 
28
- Remote Code Execution & Hosting client for **PyVault**.
28
+ Remote Code Hosting client for **PyVault**.
29
29
 
30
30
  Upload Python scripts to a PyVault server and execute them remotely — the source code **never leaves the server**. Clients only ever hold a 21-character hex Session ID.
31
31
 
@@ -47,9 +47,22 @@ sid = CodeManager.enc("my_script.py")
47
47
  # Upload from a paste service URL
48
48
  sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
49
49
 
50
- # Execute remotely (source stays on server)
50
+ # Execute in a short-lived isolated subprocess by default
51
51
  CodeManager.run(sid)
52
52
 
53
+ # Upload with a short share link and library metadata
54
+ sid = CodeManager.enc(
55
+ "my_script.py",
56
+ label="analytics demo",
57
+ libraries=["requests", "pandas"],
58
+ username="your_name",
59
+ description="A public demo post",
60
+ tags=["python", "demo"],
61
+ )
62
+
63
+ # Hide PyVault status messages while running
64
+ CodeManager.run(sid, show_terminal=False)
65
+
53
66
  # Check metadata without incrementing execution counter
54
67
  CodeManager.info(sid)
55
68
 
@@ -62,16 +75,29 @@ CodeManager.ping()
62
75
  Set `PYVAULT_URL` to point at your hosted PyVault server:
63
76
 
64
77
  ```bash
65
- export PYVAULT_URL=https://your-server.replit.app
78
+ export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
66
79
  ```
67
80
 
81
+ The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
82
+ For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
83
+ You can also control status output globally:
84
+
85
+ ```bash
86
+ export PYVAULT_TERMINAL=off
87
+ ```
88
+
89
+ Library names are metadata only. PyVault does not auto-install packages or
90
+ execute dependency installers; install the libraries in the Python environment
91
+ where `CodeManager.run()` executes.
92
+
68
93
  ## API Reference
69
94
 
70
95
  | Method | Description |
71
96
  |--------|-------------|
72
97
  | `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
98
+ | `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
73
99
  | `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
74
- | `CodeManager.run(session_id)` | Fetch and execute code by Session ID |
100
+ | `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
75
101
  | `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
76
102
  | `CodeManager.ping()` | Check server connectivity |
77
103
  | `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
@@ -102,6 +128,11 @@ export PYVAULT_URL=https://your-server.replit.app
102
128
  - Session IDs are 21-character cryptographically random hex strings
103
129
  - Source code is never written to disk on the client
104
130
  - Code is encrypted at rest with Fernet on the server
131
+ - Code runs in a short-lived subprocess by default with a clean environment,
132
+ temporary working directory, CPU/memory/file-size limits, and a hard timeout
133
+ - `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
134
+ it is not recommended for untrusted sessions
135
+ - Share links expose only session metadata and declared library names, never source
105
136
  - Admin edits require the admin access code; owner edits/deletes require the one-time owner token
106
137
  - Sessions never expire unless an expiry is explicitly selected
107
138
 
@@ -1,6 +1,6 @@
1
1
  # PyVaultRCE
2
2
 
3
- Remote Code Execution & Hosting client for **PyVault**.
3
+ Remote Code Hosting client for **PyVault**.
4
4
 
5
5
  Upload Python scripts to a PyVault server and execute them remotely — the source code **never leaves the server**. Clients only ever hold a 21-character hex Session ID.
6
6
 
@@ -22,9 +22,22 @@ sid = CodeManager.enc("my_script.py")
22
22
  # Upload from a paste service URL
23
23
  sid = CodeManager.enc_url("https://pastebin.com/abcXYZ123")
24
24
 
25
- # Execute remotely (source stays on server)
25
+ # Execute in a short-lived isolated subprocess by default
26
26
  CodeManager.run(sid)
27
27
 
28
+ # Upload with a short share link and library metadata
29
+ sid = CodeManager.enc(
30
+ "my_script.py",
31
+ label="analytics demo",
32
+ libraries=["requests", "pandas"],
33
+ username="your_name",
34
+ description="A public demo post",
35
+ tags=["python", "demo"],
36
+ )
37
+
38
+ # Hide PyVault status messages while running
39
+ CodeManager.run(sid, show_terminal=False)
40
+
28
41
  # Check metadata without incrementing execution counter
29
42
  CodeManager.info(sid)
30
43
 
@@ -37,16 +50,29 @@ CodeManager.ping()
37
50
  Set `PYVAULT_URL` to point at your hosted PyVault server:
38
51
 
39
52
  ```bash
40
- export PYVAULT_URL=https://your-server.replit.app
53
+ export PYVAULT_URL=https://secure-code-runner--diwasrepl.replit.app
41
54
  ```
42
55
 
56
+ The package uses the public PyVault deployment when `PYVAULT_URL` is not set.
57
+ For local development, explicitly use `PYVAULT_URL=http://localhost:5000`.
58
+ You can also control status output globally:
59
+
60
+ ```bash
61
+ export PYVAULT_TERMINAL=off
62
+ ```
63
+
64
+ Library names are metadata only. PyVault does not auto-install packages or
65
+ execute dependency installers; install the libraries in the Python environment
66
+ where `CodeManager.run()` executes.
67
+
43
68
  ## API Reference
44
69
 
45
70
  | Method | Description |
46
71
  |--------|-------------|
47
72
  | `CodeManager.enc(file_path)` | Upload a local `.py` file → returns Session ID |
73
+ | `CodeManager.enc(file_path, label=..., libraries=[...], username=..., description=..., tags=...)` | Upload with public profile metadata and visible library metadata |
48
74
  | `CodeManager.enc_url(url)` | Download code from a paste URL and upload → returns Session ID |
49
- | `CodeManager.run(session_id)` | Fetch and execute code by Session ID |
75
+ | `CodeManager.run(session_id, show_terminal=False, isolated=True)` | Fetch and execute code in an isolated subprocess with timeout/resource limits |
50
76
  | `CodeManager.info(session_id)` | Get session metadata (no exec count increment) |
51
77
  | `CodeManager.ping()` | Check server connectivity |
52
78
  | `CodeManager.edit(session_id, file_path, admin_token=...)` | Replace stored code (admin or owner token) |
@@ -77,6 +103,11 @@ export PYVAULT_URL=https://your-server.replit.app
77
103
  - Session IDs are 21-character cryptographically random hex strings
78
104
  - Source code is never written to disk on the client
79
105
  - Code is encrypted at rest with Fernet on the server
106
+ - Code runs in a short-lived subprocess by default with a clean environment,
107
+ temporary working directory, CPU/memory/file-size limits, and a hard timeout
108
+ - `isolated=False` is an explicit trusted-code escape hatch for shared namespaces;
109
+ it is not recommended for untrusted sessions
110
+ - Share links expose only session metadata and declared library names, never source
80
111
  - Admin edits require the admin access code; owner edits/deletes require the one-time owner token
81
112
  - Sessions never expire unless an expiry is explicitly selected
82
113
 
@@ -21,5 +21,5 @@ Usage:
21
21
  from .manager import CodeManager
22
22
 
23
23
  __all__ = ["CodeManager"]
24
- __version__ = "1.0.0"
24
+ __version__ = "2.5.0"
25
25
  __author__ = "PyVault"
@@ -3,7 +3,9 @@ manager.py — CodeManager implementation
3
3
  """
4
4
 
5
5
  import os
6
+ import subprocess
6
7
  import sys
8
+ import tempfile
7
9
  import traceback
8
10
  import textwrap
9
11
  from typing import Optional
@@ -15,7 +17,7 @@ except ImportError:
15
17
  "The 'requests' library is required. Install it with: pip install requests"
16
18
  )
17
19
 
18
- _DEFAULT_BASE_URL = "https://secure-code-runner--diwasreplit.replit.app"
20
+ _DEFAULT_BASE_URL = "https://secure-code-runner--diwasrepl.replit.app"
19
21
 
20
22
  # ── Validation helpers ───────────────────────────────────────────────────────
21
23
 
@@ -40,6 +42,70 @@ def _get_base_url(base_url: Optional[str]) -> str:
40
42
  return url
41
43
 
42
44
 
45
+ def _terminal_enabled(show_terminal=None) -> bool:
46
+ if show_terminal is not None:
47
+ return bool(show_terminal)
48
+ return os.environ.get("PYVAULT_TERMINAL", "on").strip().lower() not in {
49
+ "0", "false", "off", "no", "quiet"
50
+ }
51
+
52
+
53
+ def _emit(message: str, show_terminal=None, *, error=False) -> None:
54
+ if _terminal_enabled(show_terminal):
55
+ print(message, file=sys.stderr if error else sys.stdout, flush=True)
56
+
57
+
58
+ def _isolated_run(code: str, session_id: str, timeout: int) -> subprocess.CompletedProcess:
59
+ """Execute in a short-lived process with clean environment and limits."""
60
+ def limit_resources():
61
+ if os.name != "posix":
62
+ return
63
+ try:
64
+ import resource
65
+ cpu = max(1, min(int(timeout), 30))
66
+ # Python's runtime reserves virtual address space during startup;
67
+ # 512 MiB breaks otherwise tiny scripts on some Linux builds.
68
+ memory = 2 * 1024 * 1024 * 1024
69
+ resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
70
+ resource.setrlimit(resource.RLIMIT_AS, (memory, memory))
71
+ resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
72
+ except (ImportError, OSError, ValueError):
73
+ pass
74
+
75
+ clean_env = {
76
+ key: os.environ[key]
77
+ for key in ("PATH", "SystemRoot", "SYSTEMROOT", "TEMP", "TMP", "HOME", "LANG", "LC_ALL")
78
+ if key in os.environ
79
+ }
80
+ clean_env.update({"PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1"})
81
+ with tempfile.TemporaryDirectory(prefix="pyvault-run-") as workdir:
82
+ script_path = os.path.join(workdir, "session.py")
83
+ with open(script_path, "w", encoding="utf-8") as handle:
84
+ handle.write(code)
85
+ try:
86
+ return subprocess.run(
87
+ [sys.executable, "-I", "-u", script_path],
88
+ cwd=workdir,
89
+ env=clean_env,
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=max(1, int(timeout)),
93
+ check=False,
94
+ preexec_fn=limit_resources if os.name == "posix" else None,
95
+ )
96
+ except subprocess.TimeoutExpired as exc:
97
+ raise TimeoutError(
98
+ f"Isolated session '{session_id}' exceeded the {timeout}s execution timeout."
99
+ ) from exc
100
+
101
+
102
+ def _emit_child_output(result: subprocess.CompletedProcess) -> None:
103
+ if result.stdout:
104
+ print(result.stdout, end="", flush=True)
105
+ if result.stderr:
106
+ print(result.stderr, end="", file=sys.stderr, flush=True)
107
+
108
+
43
109
  # ── CodeManager ─────────────────────────────────────────────────────────────
44
110
 
45
111
  class CodeManager:
@@ -59,6 +125,11 @@ class CodeManager:
59
125
  file_path: str,
60
126
  base_url: Optional[str] = None,
61
127
  timeout: int = 30,
128
+ label: str = "",
129
+ libraries = "",
130
+ username: str = "",
131
+ description: str = "",
132
+ tags = "",
62
133
  ) -> str:
63
134
  """
64
135
  Read a local Python file and upload it to the PyVault backend.
@@ -98,7 +169,14 @@ class CodeManager:
98
169
  try:
99
170
  response = requests.post(
100
171
  url,
101
- json={"code": code},
172
+ json={
173
+ "code": code,
174
+ "label": label,
175
+ "libraries": libraries,
176
+ "username": username,
177
+ "description": description,
178
+ "tags": tags,
179
+ },
102
180
  timeout=timeout,
103
181
  )
104
182
  except requests.exceptions.ConnectionError:
@@ -142,12 +220,17 @@ class CodeManager:
142
220
  base_url: Optional[str] = None,
143
221
  timeout: int = 30,
144
222
  globals_dict: Optional[dict] = None,
223
+ show_terminal=None,
224
+ isolated: bool = True,
145
225
  ) -> None:
146
226
  """
147
- Fetch the code for a Session ID from PyVault and execute it locally.
227
+ Fetch the code for a Session ID from PyVault and execute it in a
228
+ short-lived isolated subprocess by default.
148
229
 
149
- The source code is fetched over the network and run via exec() —
150
- the caller never has direct access to the source file on disk.
230
+ The source code is fetched over the network. The default subprocess
231
+ uses a clean environment, a temporary working directory, and resource
232
+ limits. Use isolated=False only for trusted code that needs a shared
233
+ in-process namespace.
151
234
 
152
235
  Args:
153
236
  session_id: The 21-character hex Session ID.
@@ -208,30 +291,43 @@ class CodeManager:
208
291
  f"Server returned empty code for session '{session_id}'."
209
292
  )
210
293
 
211
- print(f"[PyVault] Executing session '{session_id}' (run #{exec_count})…")
294
+ _emit(
295
+ f"[PyVault] Executing session '{session_id}' "
296
+ f"(run #{exec_count}, isolated={isolated})…",
297
+ show_terminal,
298
+ )
212
299
 
213
- namespace = globals_dict if globals_dict is not None else {
214
- "__name__": "__pyvault__",
215
- "__builtins__": __builtins__,
216
- }
300
+ if isolated:
301
+ if globals_dict is not None:
302
+ raise ValueError("isolated=True cannot use globals_dict; pass isolated=False for a shared namespace.")
303
+ result = _isolated_run(code, session_id, timeout)
304
+ _emit_child_output(result)
305
+ if result.returncode != 0:
306
+ raise RuntimeError(
307
+ f"Isolated session '{session_id}' exited with code {result.returncode}."
308
+ )
309
+ else:
310
+ namespace = globals_dict if globals_dict is not None else {
311
+ "__name__": "__pyvault__",
312
+ "__builtins__": __builtins__,
313
+ }
314
+ try:
315
+ compiled = compile(code, f"<pyvault:{session_id}>", "exec")
316
+ exec(compiled, namespace)
317
+ except SyntaxError as exc:
318
+ _emit("\n[PyVault] ✕ Syntax error in remote code:", show_terminal, error=True)
319
+ _emit(f" Line {exc.lineno}: {exc.msg}", show_terminal, error=True)
320
+ if exc.text:
321
+ _emit(f" >>> {exc.text.strip()}", show_terminal, error=True)
322
+ raise
323
+ except Exception:
324
+ _emit("\n[PyVault] ✕ Runtime error during execution:", show_terminal, error=True)
325
+ tb_lines = traceback.format_exc().splitlines()
326
+ for line in tb_lines:
327
+ _emit(f" {line}", show_terminal, error=True)
328
+ raise
217
329
 
218
- try:
219
- compiled = compile(code, f"<pyvault:{session_id}>", "exec")
220
- exec(compiled, namespace)
221
- except SyntaxError as exc:
222
- print(f"\n[PyVault] ✕ Syntax error in remote code:", file=sys.stderr)
223
- print(f" Line {exc.lineno}: {exc.msg}", file=sys.stderr)
224
- if exc.text:
225
- print(f" >>> {exc.text.strip()}", file=sys.stderr)
226
- raise
227
- except Exception as exc:
228
- print(f"\n[PyVault] ✕ Runtime error during execution:", file=sys.stderr)
229
- tb_lines = traceback.format_exc().splitlines()
230
- for line in tb_lines:
231
- print(f" {line}", file=sys.stderr)
232
- raise
233
-
234
- print(f"[PyVault] ✓ Execution complete.")
330
+ _emit("[PyVault] ✓ Execution complete.", show_terminal)
235
331
 
236
332
  @staticmethod
237
333
  def edit(
@@ -18,5 +18,5 @@ Quick start:
18
18
  from .manager import CodeManager
19
19
 
20
20
  __all__ = ["CodeManager"]
21
- __version__ = "2.3.0"
21
+ __version__ = "2.5.0"
22
22
  __author__ = "PyVault"
@@ -4,7 +4,9 @@ manager.py — CodeManager implementation for PyVaultRCE
4
4
 
5
5
  import os
6
6
  import re
7
+ import subprocess
7
8
  import sys
9
+ import tempfile
8
10
  import traceback
9
11
  from typing import Optional
10
12
 
@@ -23,7 +25,7 @@ try:
23
25
  except ImportError:
24
26
  _HAS_CRYPTO = False
25
27
 
26
- _DEFAULT_BASE = "https://secure-code-runner--diwasreplit.replit.app"
28
+ _DEFAULT_BASE = "https://secure-code-runner--diwasrepl.replit.app"
27
29
  _MAX_LINES = 10_000
28
30
  _SID_LEN = 21
29
31
  _HEX_SET = frozenset("0123456789abcdef")
@@ -47,6 +49,33 @@ def _base(url: Optional[str]) -> str:
47
49
  return (url or os.environ.get("PYVAULT_URL") or _DEFAULT_BASE).rstrip("/")
48
50
 
49
51
 
52
+ def _terminal_enabled(show_terminal: Optional[bool]) -> bool:
53
+ if show_terminal is not None:
54
+ return bool(show_terminal)
55
+ value = os.environ.get("PYVAULT_TERMINAL", "on").strip().lower()
56
+ return value not in {"0", "false", "off", "no", "quiet"}
57
+
58
+
59
+ def _emit(message: str, show_terminal: Optional[bool] = None, *, error: bool = False) -> None:
60
+ if _terminal_enabled(show_terminal):
61
+ print(message, file=sys.stderr if error else sys.stdout, flush=True)
62
+
63
+
64
+ def _normalise_libraries(value) -> str:
65
+ if isinstance(value, (list, tuple)):
66
+ values = value
67
+ elif isinstance(value, str):
68
+ values = re.split(r"[,\n]", value)
69
+ else:
70
+ values = []
71
+ cleaned = []
72
+ for item in values:
73
+ name = str(item).strip()
74
+ if name and re.fullmatch(r"[A-Za-z0-9_.-]{1,80}", name) and name not in cleaned:
75
+ cleaned.append(name)
76
+ return ", ".join(cleaned)[:500]
77
+
78
+
50
79
  def _valid_sid(sid: str) -> bool:
51
80
  return isinstance(sid, str) and len(sid) == _SID_LEN and all(c in _HEX_SET for c in sid)
52
81
 
@@ -144,14 +173,75 @@ def _localhost_hint(url: str) -> str:
144
173
  return ""
145
174
 
146
175
 
176
+ def _isolated_run(code: str, session_id: str, timeout: int) -> subprocess.CompletedProcess:
177
+ """Run fetched code in a short-lived process with a clean environment.
178
+
179
+ This is a defense-in-depth boundary for the client. It is intentionally
180
+ not described as a container or a complete OS sandbox: callers who need
181
+ hostile-code isolation should use a dedicated VM/container policy.
182
+ """
183
+ def limit_resources():
184
+ if os.name != "posix":
185
+ return
186
+ try:
187
+ import resource
188
+ cpu = max(1, min(int(timeout), 30))
189
+ # Python's runtime reserves virtual address space during startup;
190
+ # 512 MiB breaks otherwise tiny scripts on some Linux builds.
191
+ memory = 2 * 1024 * 1024 * 1024
192
+ resource.setrlimit(resource.RLIMIT_CPU, (cpu, cpu))
193
+ resource.setrlimit(resource.RLIMIT_AS, (memory, memory))
194
+ resource.setrlimit(resource.RLIMIT_FSIZE, (10 * 1024 * 1024, 10 * 1024 * 1024))
195
+ except (ImportError, OSError, ValueError):
196
+ pass
197
+
198
+ clean_env = {}
199
+ for key in ("PATH", "SystemRoot", "SYSTEMROOT", "TEMP", "TMP", "HOME", "LANG", "LC_ALL"):
200
+ if key in os.environ:
201
+ clean_env[key] = os.environ[key]
202
+ clean_env.update({"PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1"})
203
+
204
+ with tempfile.TemporaryDirectory(prefix="pyvault-run-") as workdir:
205
+ script_path = os.path.join(workdir, "session.py")
206
+ with open(script_path, "w", encoding="utf-8") as handle:
207
+ handle.write(code)
208
+ try:
209
+ return subprocess.run(
210
+ [sys.executable, "-I", "-u", script_path],
211
+ cwd=workdir,
212
+ env=clean_env,
213
+ capture_output=True,
214
+ text=True,
215
+ timeout=max(1, int(timeout)),
216
+ check=False,
217
+ preexec_fn=limit_resources if os.name == "posix" else None,
218
+ )
219
+ except subprocess.TimeoutExpired as exc:
220
+ raise TimeoutError(
221
+ f"Isolated session '{session_id}' exceeded the {timeout}s execution timeout."
222
+ ) from exc
223
+
224
+
225
+ def _emit_child_output(result: subprocess.CompletedProcess) -> None:
226
+ if result.stdout:
227
+ print(result.stdout, end="", flush=True)
228
+ if result.stderr:
229
+ print(result.stderr, end="", file=sys.stderr, flush=True)
230
+
231
+
147
232
  def _upload_code(
148
233
  code: str,
149
234
  base_url: Optional[str],
150
235
  timeout: int,
151
236
  expires_in_hours: Optional[float] = None,
152
237
  max_executions: int = 0,
238
+ label: str = "",
239
+ libraries = "",
240
+ username: str = "",
241
+ description: str = "",
242
+ tags = "",
153
243
  ) -> tuple:
154
- """Internal: push code string to /pyv/save. Returns (session_id, owner_token)."""
244
+ """Internal: push code string to /pyv/save. Returns session metadata."""
155
245
  if not code.strip():
156
246
  raise ValueError("Code is empty — nothing to upload.")
157
247
  line_count = len(code.splitlines())
@@ -164,6 +254,16 @@ def _upload_code(
164
254
  payload["expires_in_hours"] = expires_in_hours
165
255
  if max_executions and max_executions > 0:
166
256
  payload["max_executions"] = max_executions
257
+ if label:
258
+ payload["label"] = str(label)[:200]
259
+ if libraries:
260
+ payload["libraries"] = _normalise_libraries(libraries)
261
+ if username:
262
+ payload["username"] = str(username)[:24]
263
+ if description:
264
+ payload["description"] = str(description)[:1000]
265
+ if tags:
266
+ payload["tags"] = tags
167
267
 
168
268
  url = _base(base_url) + "/pyv/save"
169
269
  resp = _http_post(url, payload, timeout)
@@ -178,7 +278,7 @@ def _upload_code(
178
278
  owner_token = data.get("owner_token", "")
179
279
  if not _valid_sid(sid):
180
280
  raise RuntimeError(f"Server returned unexpected Session ID: '{sid}'")
181
- return sid, owner_token
281
+ return sid, owner_token, data.get("share_url", ""), data.get("libraries", "")
182
282
 
183
283
 
184
284
  # ── CodeManager ───────────────────────────────────────────────────────────────
@@ -197,7 +297,8 @@ class CodeManager:
197
297
 
198
298
  Environment variables:
199
299
  PYVAULT_URL — base URL of the PyVault server
200
- (default: https://secure-code-runner--diwasreplit.replit.app)
300
+ (default: the PyVault public deployment; set PYVAULT_URL to override)
301
+ PYVAULT_TERMINAL — on/off terminal status messages (default: on)
201
302
  """
202
303
 
203
304
  @staticmethod
@@ -207,6 +308,12 @@ class CodeManager:
207
308
  timeout: int = 30,
208
309
  expires_in_hours: Optional[float] = None,
209
310
  max_executions: int = 0,
311
+ label: str = "",
312
+ libraries = "",
313
+ username: str = "",
314
+ description: str = "",
315
+ tags = "",
316
+ show_terminal: Optional[bool] = None,
210
317
  ) -> str:
211
318
  """
212
319
  Read a local .py file and upload it to PyVault.
@@ -227,14 +334,21 @@ class CodeManager:
227
334
  code = _read_file(file_path)
228
335
  if not code.strip():
229
336
  raise ValueError(f"The file '{file_path}' is empty — nothing to upload.")
230
- sid, owner_token = _upload_code(code, base_url, timeout, expires_in_hours, max_executions)
231
- print(f"[PyVault] Uploaded — Session ID : {sid}", flush=True)
232
- print(f"[PyVault] Source : {os.path.abspath(file_path)}", flush=True)
233
- print(f"[PyVault] Lines : {len(code.splitlines()):,}", flush=True)
337
+ sid, owner_token, share_url, safe_libraries = _upload_code(
338
+ code, base_url, timeout, expires_in_hours, max_executions, label, libraries,
339
+ username, description, tags
340
+ )
341
+ _emit(f"[PyVault] ✓ Uploaded — Session ID : {sid}", show_terminal)
342
+ _emit(f"[PyVault] Source : {os.path.abspath(file_path)}", show_terminal)
343
+ _emit(f"[PyVault] Lines : {len(code.splitlines()):,}", show_terminal)
344
+ if share_url:
345
+ _emit(f"[PyVault] Share link: {share_url}", show_terminal)
346
+ if safe_libraries:
347
+ _emit(f"[PyVault] Libraries: {safe_libraries}", show_terminal)
234
348
  if owner_token:
235
- print(f"\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", flush=True)
236
- print(f"[PyVault] {owner_token}", flush=True)
237
- print(f"[PyVault] Use this to delete or edit your session later.\n", flush=True)
349
+ _emit("\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", show_terminal)
350
+ _emit(f"[PyVault] {owner_token}", show_terminal)
351
+ _emit("[PyVault] Use this to delete or edit your session later.\n", show_terminal)
238
352
  return sid
239
353
 
240
354
  @staticmethod
@@ -242,6 +356,12 @@ class CodeManager:
242
356
  paste_url: str,
243
357
  base_url: Optional[str] = None,
244
358
  timeout: int = 30,
359
+ label: str = "",
360
+ libraries = "",
361
+ username: str = "",
362
+ description: str = "",
363
+ tags = "",
364
+ show_terminal: Optional[bool] = None,
245
365
  ) -> str:
246
366
  """
247
367
  Download Python code from a paste service URL and upload to PyVault.
@@ -265,7 +385,7 @@ class CodeManager:
265
385
  resp = requests.get(
266
386
  raw_url,
267
387
  timeout=timeout,
268
- headers={"User-Agent": "PyVaultRCE/2.3"},
388
+ headers={"User-Agent": "PyVaultRCE/2.5"},
269
389
  )
270
390
  resp.raise_for_status()
271
391
  except requests.exceptions.ConnectionError:
@@ -283,14 +403,21 @@ class CodeManager:
283
403
  if not code.strip():
284
404
  raise ValueError("Downloaded content is empty.")
285
405
 
286
- sid, owner_token = _upload_code(code, base_url, timeout)
287
- print(f"[PyVault] Uploaded from URL — Session ID : {sid}", flush=True)
288
- print(f"[PyVault] Source : {paste_url}", flush=True)
289
- print(f"[PyVault] Lines : {len(code.splitlines()):,}", flush=True)
406
+ sid, owner_token, share_url, safe_libraries = _upload_code(
407
+ code, base_url, timeout, label=label, libraries=libraries,
408
+ username=username, description=description, tags=tags
409
+ )
410
+ _emit(f"[PyVault] ✓ Uploaded from URL — Session ID : {sid}", show_terminal)
411
+ _emit(f"[PyVault] Source : {paste_url}", show_terminal)
412
+ _emit(f"[PyVault] Lines : {len(code.splitlines()):,}", show_terminal)
413
+ if share_url:
414
+ _emit(f"[PyVault] Share link: {share_url}", show_terminal)
415
+ if safe_libraries:
416
+ _emit(f"[PyVault] Libraries: {safe_libraries}", show_terminal)
290
417
  if owner_token:
291
- print(f"\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", flush=True)
292
- print(f"[PyVault] {owner_token}", flush=True)
293
- print(f"[PyVault] Use this to delete or edit your session later.\n", flush=True)
418
+ _emit("\n[PyVault] ⚠ OWNER TOKEN (save this — shown only once!):", show_terminal)
419
+ _emit(f"[PyVault] {owner_token}", show_terminal)
420
+ _emit("[PyVault] Use this to delete or edit your session later.\n", show_terminal)
294
421
  return sid
295
422
 
296
423
  @staticmethod
@@ -299,20 +426,27 @@ class CodeManager:
299
426
  base_url: Optional[str] = None,
300
427
  timeout: int = 30,
301
428
  _ns: Optional[dict] = None,
429
+ show_terminal: Optional[bool] = None,
430
+ isolated: bool = True,
302
431
  ) -> None:
303
432
  """
304
433
  Fetch the encrypted code for a Session ID from PyVault, decrypt it,
305
- and execute it locally.
434
+ and execute it in a short-lived isolated subprocess by default.
306
435
 
307
- The source code is transmitted encrypted, decrypted in-memory, never
308
- written to disk, and is not accessible after execution.
436
+ The source code is transmitted encrypted and decrypted in memory. The
437
+ default subprocess writes it only to a temporary file for the child
438
+ process, then removes that directory after execution.
309
439
 
310
440
  Args:
311
441
  session_id: The 21-character hex Session ID.
312
442
  base_url: Override the server URL.
313
443
  timeout: HTTP timeout in seconds.
314
444
  _ns: Optional namespace dict passed to exec(). Leave as None
315
- unless you intentionally want to share a namespace.
445
+ unless you intentionally want to share a namespace;
446
+ providing it requires isolated=False.
447
+ isolated: Use the short-lived subprocess boundary (default True).
448
+ Set False only for trusted code that needs an in-process
449
+ namespace.
316
450
 
317
451
  Raises:
318
452
  ValueError, ConnectionError, RuntimeError, plus any exception
@@ -363,28 +497,41 @@ class CodeManager:
363
497
  if not code.strip():
364
498
  raise RuntimeError(f"Decrypted code is empty for session '{session_id}'.")
365
499
 
366
- print(f"[PyVault] ▶ Running session '{session_id}' (execution #{run_count})…", flush=True)
367
-
368
- namespace = _ns if _ns is not None else {
369
- "__name__": "__pyvault__",
370
- "__builtins__": __builtins__,
371
- }
500
+ _emit(
501
+ f"[PyVault] ▶ Running session '{session_id}' "
502
+ f"(execution #{run_count}, isolated={isolated})…",
503
+ show_terminal,
504
+ )
372
505
 
373
- try:
374
- exec(compile(code, f"<vault:{session_id[:8]}…>", "exec"), namespace)
375
- except SyntaxError as exc:
376
- print(f"\n[PyVault] Syntax error:", file=sys.stderr, flush=True)
377
- print(f" Line {exc.lineno}: {exc.msg}", file=sys.stderr)
378
- if exc.text:
379
- print(f" >>> {exc.text.strip()}", file=sys.stderr)
380
- raise
381
- except Exception:
382
- print(f"\n[PyVault] ✕ Runtime error:", file=sys.stderr, flush=True)
383
- for line in traceback.format_exc().splitlines():
384
- print(f" {line}", file=sys.stderr)
385
- raise
506
+ if isolated:
507
+ if _ns is not None:
508
+ raise ValueError("isolated=True cannot use _ns; pass isolated=False for a shared namespace.")
509
+ result = _isolated_run(code, session_id, timeout)
510
+ _emit_child_output(result)
511
+ if result.returncode != 0:
512
+ raise RuntimeError(
513
+ f"Isolated session '{session_id}' exited with code {result.returncode}."
514
+ )
515
+ else:
516
+ namespace = _ns if _ns is not None else {
517
+ "__name__": "__pyvault__",
518
+ "__builtins__": __builtins__,
519
+ }
520
+ try:
521
+ exec(compile(code, f"<vault:{session_id[:8]}…>", "exec"), namespace)
522
+ except SyntaxError as exc:
523
+ _emit("\n[PyVault] ✕ Syntax error:", show_terminal, error=True)
524
+ _emit(f" Line {exc.lineno}: {exc.msg}", show_terminal, error=True)
525
+ if exc.text:
526
+ _emit(f" >>> {exc.text.strip()}", show_terminal, error=True)
527
+ raise
528
+ except Exception:
529
+ _emit("\n[PyVault] ✕ Runtime error:", show_terminal, error=True)
530
+ for line in traceback.format_exc().splitlines():
531
+ _emit(f" {line}", show_terminal, error=True)
532
+ raise
386
533
 
387
- print(f"[PyVault] ✓ Execution complete.", flush=True)
534
+ _emit("[PyVault] ✓ Execution complete.", show_terminal)
388
535
 
389
536
  @staticmethod
390
537
  def info(
@@ -426,6 +573,19 @@ class CodeManager:
426
573
  )
427
574
  return data
428
575
 
576
+ @staticmethod
577
+ def share(
578
+ session_id: str,
579
+ base_url: Optional[str] = None,
580
+ timeout: int = 30,
581
+ ) -> str:
582
+ """Return the short public metadata link for a session."""
583
+ data = CodeManager.info(session_id, base_url=base_url, timeout=timeout)
584
+ share_url = data.get("share_url", "")
585
+ if not share_url:
586
+ raise RuntimeError("This server did not return a share URL.")
587
+ return share_url
588
+
429
589
  @staticmethod
430
590
  def ping(
431
591
  base_url: Optional[str] = None,
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="PyVaultRCE",
8
- version="2.3.0",
8
+ version="2.5.0",
9
9
  author="PyVault",
10
10
  description="Remote Code Execution & Hosting client for PyVault — source never exposed",
11
11
  long_description=long_description,
File without changes