pactown 0.1.4__py3-none-any.whl → 0.1.47__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.
- pactown/__init__.py +178 -4
- pactown/cli.py +539 -37
- pactown/config.py +12 -11
- pactown/deploy/__init__.py +17 -3
- pactown/deploy/base.py +35 -33
- pactown/deploy/compose.py +59 -58
- pactown/deploy/docker.py +40 -41
- pactown/deploy/kubernetes.py +43 -42
- pactown/deploy/podman.py +55 -56
- pactown/deploy/quadlet.py +1021 -0
- pactown/deploy/quadlet_api.py +533 -0
- pactown/deploy/quadlet_shell.py +557 -0
- pactown/events.py +1066 -0
- pactown/fast_start.py +514 -0
- pactown/generator.py +31 -30
- pactown/llm.py +450 -0
- pactown/markpact_blocks.py +50 -0
- pactown/network.py +59 -38
- pactown/orchestrator.py +90 -93
- pactown/parallel.py +40 -40
- pactown/platform.py +146 -0
- pactown/registry/__init__.py +1 -1
- pactown/registry/client.py +45 -46
- pactown/registry/models.py +25 -25
- pactown/registry/server.py +24 -24
- pactown/resolver.py +30 -30
- pactown/runner_api.py +458 -0
- pactown/sandbox_manager.py +480 -79
- pactown/security.py +682 -0
- pactown/service_runner.py +1201 -0
- pactown/user_isolation.py +458 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/METADATA +65 -9
- pactown-0.1.47.dist-info/RECORD +36 -0
- pactown-0.1.47.dist-info/entry_points.txt +5 -0
- pactown-0.1.4.dist-info/RECORD +0 -24
- pactown-0.1.4.dist-info/entry_points.txt +0 -3
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/WHEEL +0 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User isolation module for pactown.
|
|
3
|
+
|
|
4
|
+
Provides Linux user-based sandbox isolation for multi-tenant SaaS:
|
|
5
|
+
- Create isolated Linux users per SaaS user
|
|
6
|
+
- Run sandboxes under isolated user accounts
|
|
7
|
+
- Easy migration of projects between hosts
|
|
8
|
+
- Resource limits via cgroups (optional)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import grp
|
|
12
|
+
import os
|
|
13
|
+
import pwd
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import logging
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Dict, List, Optional, Any
|
|
21
|
+
from threading import Lock
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("pactown.isolation")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sanitize_gecos(value: str) -> str:
|
|
27
|
+
v = str(value or "")
|
|
28
|
+
v = v.replace(":", "_")
|
|
29
|
+
v = re.sub(r"[\x00\r\n\t]", " ", v)
|
|
30
|
+
v = re.sub(r"\s+", " ", v).strip()
|
|
31
|
+
if not v:
|
|
32
|
+
v = "pactown-user"
|
|
33
|
+
return v[:200]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class IsolatedUser:
|
|
38
|
+
"""Represents an isolated Linux user for sandbox execution."""
|
|
39
|
+
saas_user_id: str
|
|
40
|
+
linux_username: str
|
|
41
|
+
linux_uid: int
|
|
42
|
+
linux_gid: int
|
|
43
|
+
home_dir: Path
|
|
44
|
+
created_at: float = field(default_factory=lambda: __import__('time').time())
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"saas_user_id": self.saas_user_id,
|
|
49
|
+
"linux_username": self.linux_username,
|
|
50
|
+
"linux_uid": self.linux_uid,
|
|
51
|
+
"linux_gid": self.linux_gid,
|
|
52
|
+
"home_dir": str(self.home_dir),
|
|
53
|
+
"created_at": self.created_at,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UserIsolationManager:
|
|
58
|
+
"""
|
|
59
|
+
Manages isolated Linux users for sandbox execution.
|
|
60
|
+
|
|
61
|
+
Each SaaS user gets a dedicated Linux user account:
|
|
62
|
+
- Username: pactown_<hash(saas_user_id)>
|
|
63
|
+
- Home dir: /home/pactown_users/<username>
|
|
64
|
+
- All sandboxes run under this user
|
|
65
|
+
|
|
66
|
+
Benefits:
|
|
67
|
+
- Process isolation (different UIDs)
|
|
68
|
+
- File system isolation (home directories)
|
|
69
|
+
- Easy migration (tar user's home dir)
|
|
70
|
+
- Resource limits via cgroups
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
PREFIX = "pactown_"
|
|
74
|
+
BASE_UID = 60000 # Start UIDs from 60000
|
|
75
|
+
BASE_GID = 60000
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
users_base: Path = Path("/home/pactown_users"),
|
|
80
|
+
enable_cgroups: bool = False,
|
|
81
|
+
):
|
|
82
|
+
self.users_base = users_base
|
|
83
|
+
self.enable_cgroups = enable_cgroups
|
|
84
|
+
self._users: Dict[str, IsolatedUser] = {}
|
|
85
|
+
self._lock = Lock()
|
|
86
|
+
self._next_uid = self.BASE_UID
|
|
87
|
+
|
|
88
|
+
# Create base directory if running as root
|
|
89
|
+
if os.geteuid() == 0:
|
|
90
|
+
self.users_base.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
# Load existing users
|
|
93
|
+
self._load_existing_users()
|
|
94
|
+
|
|
95
|
+
def can_isolate(self) -> tuple[bool, str]:
|
|
96
|
+
if os.geteuid() != 0:
|
|
97
|
+
return False, "not running as root"
|
|
98
|
+
if shutil.which("useradd") is None:
|
|
99
|
+
return False, "missing 'useradd' (install 'passwd'/'shadow' tools)"
|
|
100
|
+
if shutil.which("groupadd") is None:
|
|
101
|
+
return False, "missing 'groupadd' (install 'passwd'/'shadow' tools)"
|
|
102
|
+
try:
|
|
103
|
+
self.users_base.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return False, f"cannot create users_base={self.users_base}: {e}"
|
|
106
|
+
try:
|
|
107
|
+
if not os.access(self.users_base, os.W_OK | os.X_OK):
|
|
108
|
+
return False, f"users_base not writable: {self.users_base}"
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return False, f"cannot check permissions for users_base={self.users_base}: {e}"
|
|
111
|
+
return True, ""
|
|
112
|
+
|
|
113
|
+
def _load_existing_users(self):
|
|
114
|
+
"""Load existing pactown users from system."""
|
|
115
|
+
try:
|
|
116
|
+
for entry in pwd.getpwall():
|
|
117
|
+
if entry.pw_name.startswith(self.PREFIX):
|
|
118
|
+
# Extract saas_user_id from comment field or username
|
|
119
|
+
saas_id = entry.pw_gecos or entry.pw_name[len(self.PREFIX):]
|
|
120
|
+
self._users[saas_id] = IsolatedUser(
|
|
121
|
+
saas_user_id=saas_id,
|
|
122
|
+
linux_username=entry.pw_name,
|
|
123
|
+
linux_uid=entry.pw_uid,
|
|
124
|
+
linux_gid=entry.pw_gid,
|
|
125
|
+
home_dir=Path(entry.pw_dir),
|
|
126
|
+
)
|
|
127
|
+
if entry.pw_uid >= self._next_uid:
|
|
128
|
+
self._next_uid = entry.pw_uid + 1
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning(f"Could not load existing users: {e}")
|
|
131
|
+
|
|
132
|
+
def _generate_username(self, saas_user_id: str) -> str:
|
|
133
|
+
"""Generate Linux username from SaaS user ID."""
|
|
134
|
+
import hashlib
|
|
135
|
+
hash_suffix = hashlib.sha256(saas_user_id.encode()).hexdigest()[:8]
|
|
136
|
+
return f"{self.PREFIX}{hash_suffix}"
|
|
137
|
+
|
|
138
|
+
def get_or_create_user(self, saas_user_id: str) -> IsolatedUser:
|
|
139
|
+
"""Get or create an isolated Linux user for a SaaS user."""
|
|
140
|
+
with self._lock:
|
|
141
|
+
if saas_user_id in self._users:
|
|
142
|
+
return self._users[saas_user_id]
|
|
143
|
+
|
|
144
|
+
# Create new user
|
|
145
|
+
username = self._generate_username(saas_user_id)
|
|
146
|
+
uid = self._next_uid
|
|
147
|
+
gid = self._next_uid
|
|
148
|
+
home_dir = self.users_base / username
|
|
149
|
+
|
|
150
|
+
# If user already exists (deterministic username), reuse it.
|
|
151
|
+
try:
|
|
152
|
+
existing = pwd.getpwnam(username)
|
|
153
|
+
user = IsolatedUser(
|
|
154
|
+
saas_user_id=saas_user_id,
|
|
155
|
+
linux_username=existing.pw_name,
|
|
156
|
+
linux_uid=existing.pw_uid,
|
|
157
|
+
linux_gid=existing.pw_gid,
|
|
158
|
+
home_dir=Path(existing.pw_dir),
|
|
159
|
+
)
|
|
160
|
+
self._users[saas_user_id] = user
|
|
161
|
+
if existing.pw_uid >= self._next_uid:
|
|
162
|
+
self._next_uid = existing.pw_uid + 1
|
|
163
|
+
logger.info(
|
|
164
|
+
"Reusing existing Linux user %s (uid=%s) for %s",
|
|
165
|
+
user.linux_username,
|
|
166
|
+
user.linux_uid,
|
|
167
|
+
saas_user_id,
|
|
168
|
+
)
|
|
169
|
+
return user
|
|
170
|
+
except KeyError:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
# Check if we can create users (requires root)
|
|
174
|
+
if os.geteuid() != 0:
|
|
175
|
+
# Non-root mode: create virtual user for tracking
|
|
176
|
+
logger.warning(f"Not running as root, creating virtual user for {saas_user_id}")
|
|
177
|
+
user = IsolatedUser(
|
|
178
|
+
saas_user_id=saas_user_id,
|
|
179
|
+
linux_username=username,
|
|
180
|
+
linux_uid=os.getuid(), # Use current user
|
|
181
|
+
linux_gid=os.getgid(),
|
|
182
|
+
home_dir=home_dir,
|
|
183
|
+
)
|
|
184
|
+
self._users[saas_user_id] = user
|
|
185
|
+
|
|
186
|
+
# Create home directory anyway
|
|
187
|
+
try:
|
|
188
|
+
home_dir.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
fallback_base = Path("/tmp/pactown_users")
|
|
191
|
+
fallback_home = fallback_base / username
|
|
192
|
+
logger.warning(
|
|
193
|
+
"Could not create users_base home_dir=%s (%s); falling back to %s",
|
|
194
|
+
home_dir,
|
|
195
|
+
e,
|
|
196
|
+
fallback_home,
|
|
197
|
+
)
|
|
198
|
+
fallback_home.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
user.home_dir = fallback_home
|
|
200
|
+
return user
|
|
201
|
+
|
|
202
|
+
can_isolate, reason = self.can_isolate()
|
|
203
|
+
if not can_isolate:
|
|
204
|
+
raise RuntimeError(f"User isolation unavailable: {reason}")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Create group
|
|
208
|
+
try:
|
|
209
|
+
grp.getgrnam(username)
|
|
210
|
+
except KeyError:
|
|
211
|
+
res = subprocess.run(
|
|
212
|
+
["groupadd", "-g", str(gid), username],
|
|
213
|
+
check=True,
|
|
214
|
+
capture_output=True,
|
|
215
|
+
text=True,
|
|
216
|
+
)
|
|
217
|
+
if res.stdout:
|
|
218
|
+
logger.debug("groupadd stdout: %s", res.stdout.strip())
|
|
219
|
+
if res.stderr:
|
|
220
|
+
logger.debug("groupadd stderr: %s", res.stderr.strip())
|
|
221
|
+
|
|
222
|
+
# Create user
|
|
223
|
+
safe_comment = _sanitize_gecos(saas_user_id)
|
|
224
|
+
res = subprocess.run(
|
|
225
|
+
[
|
|
226
|
+
"useradd",
|
|
227
|
+
"-u",
|
|
228
|
+
str(uid),
|
|
229
|
+
"-g",
|
|
230
|
+
str(gid),
|
|
231
|
+
"-d",
|
|
232
|
+
str(home_dir),
|
|
233
|
+
"-m",
|
|
234
|
+
"-s",
|
|
235
|
+
"/bin/bash",
|
|
236
|
+
"-c",
|
|
237
|
+
safe_comment,
|
|
238
|
+
username,
|
|
239
|
+
],
|
|
240
|
+
check=True,
|
|
241
|
+
capture_output=True,
|
|
242
|
+
text=True,
|
|
243
|
+
)
|
|
244
|
+
if res.stdout:
|
|
245
|
+
logger.debug("useradd stdout: %s", res.stdout.strip())
|
|
246
|
+
if res.stderr:
|
|
247
|
+
logger.debug("useradd stderr: %s", res.stderr.strip())
|
|
248
|
+
|
|
249
|
+
logger.info(f"Created Linux user {username} (uid={uid}) for {saas_user_id}")
|
|
250
|
+
|
|
251
|
+
except subprocess.CalledProcessError as e:
|
|
252
|
+
stderr = getattr(e, "stderr", None)
|
|
253
|
+
stdout = getattr(e, "stdout", None)
|
|
254
|
+
logger.error(
|
|
255
|
+
"Failed to create isolated user for %s: cmd=%s returncode=%s stdout=%s stderr=%s",
|
|
256
|
+
saas_user_id,
|
|
257
|
+
getattr(e, "cmd", None),
|
|
258
|
+
getattr(e, "returncode", None),
|
|
259
|
+
(stdout.strip() if isinstance(stdout, str) else stdout),
|
|
260
|
+
(stderr.strip() if isinstance(stderr, str) else stderr),
|
|
261
|
+
)
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
f"Failed to create isolated user (saas_user_id={saas_user_id}, username={username}): {e}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
user = IsolatedUser(
|
|
267
|
+
saas_user_id=saas_user_id,
|
|
268
|
+
linux_username=username,
|
|
269
|
+
linux_uid=uid,
|
|
270
|
+
linux_gid=gid,
|
|
271
|
+
home_dir=home_dir,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
self._users[saas_user_id] = user
|
|
275
|
+
self._next_uid += 1
|
|
276
|
+
|
|
277
|
+
return user
|
|
278
|
+
|
|
279
|
+
def get_user(self, saas_user_id: str) -> Optional[IsolatedUser]:
|
|
280
|
+
"""Get isolated user without creating."""
|
|
281
|
+
return self._users.get(saas_user_id)
|
|
282
|
+
|
|
283
|
+
def get_sandbox_path(self, saas_user_id: str, service_id: str) -> Path:
|
|
284
|
+
"""Get sandbox path for a specific service under user's home."""
|
|
285
|
+
user = self.get_or_create_user(saas_user_id)
|
|
286
|
+
sandbox_dir = user.home_dir / "sandboxes" / service_id
|
|
287
|
+
sandbox_dir.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
|
|
289
|
+
# Set ownership if root
|
|
290
|
+
if os.geteuid() == 0:
|
|
291
|
+
os.chown(sandbox_dir, user.linux_uid, user.linux_gid)
|
|
292
|
+
|
|
293
|
+
return sandbox_dir
|
|
294
|
+
|
|
295
|
+
def run_as_user(
|
|
296
|
+
self,
|
|
297
|
+
saas_user_id: str,
|
|
298
|
+
command: str,
|
|
299
|
+
cwd: Path,
|
|
300
|
+
env: Optional[Dict[str, str]] = None,
|
|
301
|
+
) -> subprocess.Popen:
|
|
302
|
+
"""
|
|
303
|
+
Run a command as the isolated user.
|
|
304
|
+
|
|
305
|
+
Returns subprocess.Popen for the running process.
|
|
306
|
+
"""
|
|
307
|
+
user = self.get_or_create_user(saas_user_id)
|
|
308
|
+
|
|
309
|
+
full_env = os.environ.copy()
|
|
310
|
+
full_env["HOME"] = str(user.home_dir)
|
|
311
|
+
full_env["USER"] = user.linux_username
|
|
312
|
+
full_env["LOGNAME"] = user.linux_username
|
|
313
|
+
if env:
|
|
314
|
+
full_env.update(env)
|
|
315
|
+
|
|
316
|
+
def set_user():
|
|
317
|
+
"""Pre-exec function to switch user."""
|
|
318
|
+
if os.geteuid() == 0:
|
|
319
|
+
os.setgid(user.linux_gid)
|
|
320
|
+
os.setuid(user.linux_uid)
|
|
321
|
+
|
|
322
|
+
process = subprocess.Popen(
|
|
323
|
+
command,
|
|
324
|
+
shell=True,
|
|
325
|
+
cwd=str(cwd),
|
|
326
|
+
env=full_env,
|
|
327
|
+
stdout=subprocess.PIPE,
|
|
328
|
+
stderr=subprocess.PIPE,
|
|
329
|
+
preexec_fn=set_user if os.geteuid() == 0 else None,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
logger.info(f"Started process {process.pid} as user {user.linux_username}")
|
|
333
|
+
return process
|
|
334
|
+
|
|
335
|
+
def list_users(self) -> List[IsolatedUser]:
|
|
336
|
+
"""List all isolated users."""
|
|
337
|
+
return list(self._users.values())
|
|
338
|
+
|
|
339
|
+
def get_user_stats(self, saas_user_id: str) -> Dict[str, Any]:
|
|
340
|
+
"""Get stats for a user's sandboxes."""
|
|
341
|
+
user = self.get_user(saas_user_id)
|
|
342
|
+
if not user:
|
|
343
|
+
return {"error": "User not found"}
|
|
344
|
+
|
|
345
|
+
sandboxes_dir = user.home_dir / "sandboxes"
|
|
346
|
+
if not sandboxes_dir.exists():
|
|
347
|
+
return {
|
|
348
|
+
"user": user.to_dict(),
|
|
349
|
+
"sandbox_count": 0,
|
|
350
|
+
"total_size_mb": 0,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
sandbox_count = len(list(sandboxes_dir.iterdir()))
|
|
354
|
+
total_size = sum(
|
|
355
|
+
f.stat().st_size
|
|
356
|
+
for f in sandboxes_dir.rglob("*")
|
|
357
|
+
if f.is_file()
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
"user": user.to_dict(),
|
|
362
|
+
"sandbox_count": sandbox_count,
|
|
363
|
+
"total_size_mb": total_size / (1024 * 1024),
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
def export_user_data(self, saas_user_id: str, output_path: Path) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Export user's data for migration.
|
|
369
|
+
|
|
370
|
+
Creates a tar.gz archive of the user's home directory.
|
|
371
|
+
"""
|
|
372
|
+
user = self.get_user(saas_user_id)
|
|
373
|
+
if not user or not user.home_dir.exists():
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
subprocess.run(
|
|
378
|
+
["tar", "-czf", str(output_path), "-C", str(user.home_dir.parent), user.linux_username],
|
|
379
|
+
check=True,
|
|
380
|
+
capture_output=True,
|
|
381
|
+
)
|
|
382
|
+
logger.info(f"Exported user {saas_user_id} to {output_path}")
|
|
383
|
+
return True
|
|
384
|
+
except subprocess.CalledProcessError as e:
|
|
385
|
+
logger.error(f"Failed to export user: {e}")
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
def import_user_data(self, saas_user_id: str, archive_path: Path) -> bool:
|
|
389
|
+
"""
|
|
390
|
+
Import user's data from migration archive.
|
|
391
|
+
"""
|
|
392
|
+
user = self.get_or_create_user(saas_user_id)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
# Extract to user's home
|
|
396
|
+
subprocess.run(
|
|
397
|
+
["tar", "-xzf", str(archive_path), "-C", str(self.users_base)],
|
|
398
|
+
check=True,
|
|
399
|
+
capture_output=True,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Fix ownership
|
|
403
|
+
if os.geteuid() == 0:
|
|
404
|
+
subprocess.run(
|
|
405
|
+
["chown", "-R", f"{user.linux_uid}:{user.linux_gid}", str(user.home_dir)],
|
|
406
|
+
check=True,
|
|
407
|
+
capture_output=True,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
logger.info(f"Imported user {saas_user_id} from {archive_path}")
|
|
411
|
+
return True
|
|
412
|
+
except subprocess.CalledProcessError as e:
|
|
413
|
+
logger.error(f"Failed to import user: {e}")
|
|
414
|
+
return False
|
|
415
|
+
|
|
416
|
+
def delete_user(self, saas_user_id: str, delete_home: bool = True) -> bool:
|
|
417
|
+
"""Delete an isolated user."""
|
|
418
|
+
user = self.get_user(saas_user_id)
|
|
419
|
+
if not user:
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
if os.geteuid() == 0:
|
|
424
|
+
# Delete Linux user
|
|
425
|
+
cmd = ["userdel"]
|
|
426
|
+
if delete_home:
|
|
427
|
+
cmd.append("-r")
|
|
428
|
+
cmd.append(user.linux_username)
|
|
429
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
|
430
|
+
subprocess.run(
|
|
431
|
+
["groupdel", user.linux_username],
|
|
432
|
+
capture_output=True, # May fail if group doesn't exist
|
|
433
|
+
)
|
|
434
|
+
elif delete_home and user.home_dir.exists():
|
|
435
|
+
# Non-root: just delete home directory
|
|
436
|
+
shutil.rmtree(user.home_dir)
|
|
437
|
+
|
|
438
|
+
del self._users[saas_user_id]
|
|
439
|
+
logger.info(f"Deleted user {saas_user_id}")
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
except subprocess.CalledProcessError as e:
|
|
443
|
+
logger.error(f"Failed to delete user: {e}")
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Global isolation manager instance
|
|
448
|
+
_isolation_manager: Optional[UserIsolationManager] = None
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def get_isolation_manager() -> UserIsolationManager:
|
|
452
|
+
"""Get global isolation manager instance."""
|
|
453
|
+
global _isolation_manager
|
|
454
|
+
if _isolation_manager is None:
|
|
455
|
+
default_base = "/home/pactown_users" if os.geteuid() == 0 else "/tmp/pactown_users"
|
|
456
|
+
users_base = Path(os.environ.get("PACTOWN_USERS_BASE", default_base))
|
|
457
|
+
_isolation_manager = UserIsolationManager(users_base=users_base)
|
|
458
|
+
return _isolation_manager
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pactown
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.47
|
|
4
4
|
Summary: Decentralized Service Ecosystem Orchestrator - Build interconnected microservices from Markdown using markpact
|
|
5
5
|
Project-URL: Homepage, https://github.com/wronai/pactown
|
|
6
6
|
Project-URL: Repository, https://github.com/wronai/pactown
|
|
@@ -30,6 +30,8 @@ Requires-Dist: pyyaml>=6.0
|
|
|
30
30
|
Requires-Dist: rich>=13.0
|
|
31
31
|
Requires-Dist: uvicorn>=0.20.0
|
|
32
32
|
Requires-Dist: watchfiles>=0.20.0
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: lolm>=0.1.6; extra == 'all'
|
|
33
35
|
Provides-Extra: dev
|
|
34
36
|
Requires-Dist: build; extra == 'dev'
|
|
35
37
|
Requires-Dist: bump2version>=1.0; extra == 'dev'
|
|
@@ -38,6 +40,8 @@ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
|
38
40
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
39
41
|
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
40
42
|
Requires-Dist: twine; extra == 'dev'
|
|
43
|
+
Provides-Extra: llm
|
|
44
|
+
Requires-Dist: lolm>=0.1.6; extra == 'llm'
|
|
41
45
|
Description-Content-Type: text/markdown
|
|
42
46
|
|
|
43
47
|

|
|
@@ -74,6 +78,7 @@ Pactown enables you to compose multiple independent markpact projects into a uni
|
|
|
74
78
|
|
|
75
79
|
## Key Features
|
|
76
80
|
|
|
81
|
+
### Core Features
|
|
77
82
|
- **🔗 Service Composition** – Combine multiple markpact READMEs into one ecosystem
|
|
78
83
|
- **📦 Local Registry** – Store and share markpact artifacts across projects
|
|
79
84
|
- **🔄 Dependency Resolution** – Automatic startup order based on service dependencies
|
|
@@ -84,14 +89,43 @@ Pactown enables you to compose multiple independent markpact projects into a uni
|
|
|
84
89
|
- **🔍 Service Discovery** – Name-based service lookup, no hardcoded URLs
|
|
85
90
|
- **⚡ Config Generator** – Auto-generate config from folder of READMEs
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
### New in v0.4.0
|
|
93
|
+
- **⚡ Fast Start** – Dependency caching for millisecond startup times ([docs](docs/FAST_START.md))
|
|
94
|
+
- **🛡️ Security Policy** – Rate limiting, user profiles, anomaly logging ([docs](docs/SECURITY_POLICY.md))
|
|
95
|
+
- **👤 User Isolation** – Linux user-based sandbox isolation for multi-tenant SaaS ([docs](docs/USER_ISOLATION.md))
|
|
96
|
+
- **📊 Detailed Logging** – Structured logs with error capture ([docs](docs/LOGGING.md))
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 📚 Documentation
|
|
101
|
+
|
|
102
|
+
### Quick Navigation
|
|
103
|
+
|
|
104
|
+
| Category | Documents |
|
|
105
|
+
|----------|-----------|
|
|
106
|
+
| **Getting Started** | [Quick Start](#quick-start) · [Installation](#installation) · [Commands](#commands) |
|
|
107
|
+
| **Core Concepts** | [Specification](docs/SPECIFICATION.md) · [Configuration](docs/CONFIGURATION.md) · [Network](docs/NETWORK.md) |
|
|
108
|
+
| **Deployment** | [Deployment Guide](docs/DEPLOYMENT.md) · [Quadlet/VPS](docs/QUADLET.md) · [Generator](docs/GENERATOR.md) |
|
|
109
|
+
| **Security** | [Security Policy](docs/SECURITY_POLICY.md) · [Quadlet Security](docs/SECURITY.md) · [User Isolation](docs/USER_ISOLATION.md) |
|
|
110
|
+
| **Performance** | [Fast Start](docs/FAST_START.md) · [Logging](docs/LOGGING.md) |
|
|
111
|
+
| **Comparisons** | [vs Cloudflare Workers](docs/CLOUDFLARE_WORKERS_COMPARISON.md) |
|
|
112
|
+
|
|
113
|
+
### All Documentation
|
|
88
114
|
|
|
89
115
|
| Document | Description |
|
|
90
116
|
|----------|-------------|
|
|
91
117
|
| [Specification](docs/SPECIFICATION.md) | Architecture and design |
|
|
92
118
|
| [Configuration](docs/CONFIGURATION.md) | YAML config reference |
|
|
119
|
+
| [Deployment](docs/DEPLOYMENT.md) | Production deployment guide (Compose/Kubernetes/Quadlet) |
|
|
93
120
|
| [Network](docs/NETWORK.md) | Dynamic ports & service discovery |
|
|
94
121
|
| [Generator](docs/GENERATOR.md) | Auto-generate configs |
|
|
122
|
+
| [Quadlet](docs/QUADLET.md) | Podman Quadlet deployment for VPS production |
|
|
123
|
+
| [Security](docs/SECURITY.md) | Quadlet security hardening and injection test suite |
|
|
124
|
+
| [Security Policy](docs/SECURITY_POLICY.md) | Rate limiting, user profiles, resource monitoring |
|
|
125
|
+
| [Fast Start](docs/FAST_START.md) | Dependency caching for fast startup |
|
|
126
|
+
| [User Isolation](docs/USER_ISOLATION.md) | Linux user-based sandbox isolation |
|
|
127
|
+
| [Logging](docs/LOGGING.md) | Structured logging and error capture |
|
|
128
|
+
| [Cloudflare Workers comparison](docs/CLOUDFLARE_WORKERS_COMPARISON.md) | When to use Pactown vs Cloudflare Workers |
|
|
95
129
|
|
|
96
130
|
### Source Code Reference
|
|
97
131
|
|
|
@@ -102,7 +136,29 @@ Pactown enables you to compose multiple independent markpact projects into a uni
|
|
|
102
136
|
| [`resolver.py`](src/pactown/resolver.py) | Dependency resolution |
|
|
103
137
|
| [`network.py`](src/pactown/network.py) | Port allocation & discovery |
|
|
104
138
|
| [`generator.py`](src/pactown/generator.py) | Config file generator |
|
|
139
|
+
| [`service_runner.py`](src/pactown/service_runner.py) | High-level service runner API |
|
|
140
|
+
| [`security.py`](src/pactown/security.py) | Security policy & rate limiting |
|
|
141
|
+
| [`fast_start.py`](src/pactown/fast_start.py) | Dependency caching & fast startup |
|
|
142
|
+
| [`user_isolation.py`](src/pactown/user_isolation.py) | Linux user isolation for multi-tenant |
|
|
143
|
+
| [`sandbox_manager.py`](src/pactown/sandbox_manager.py) | Sandbox lifecycle management |
|
|
105
144
|
| [`registry/`](src/pactown/registry/) | Local artifact registry |
|
|
145
|
+
| [`deploy/`](src/pactown/deploy/) | Deployment backends (Docker, Podman, K8s, Quadlet) |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 🎯 Examples
|
|
150
|
+
|
|
151
|
+
| Example | What it shows |
|
|
152
|
+
|---------|---------------|
|
|
153
|
+
| [`examples/saas-platform/`](examples/saas-platform/) | Complete SaaS with Web + API + Database + Gateway |
|
|
154
|
+
| [`examples/quadlet-vps/`](examples/quadlet-vps/) | VPS setup and Quadlet workflow |
|
|
155
|
+
| [`examples/email-llm-responder/`](examples/email-llm-responder/) | Email automation with LLM integration |
|
|
156
|
+
| [`examples/api-gateway-webhooks/`](examples/api-gateway-webhooks/) | API gateway / webhook handler |
|
|
157
|
+
| [`examples/realtime-notifications/`](examples/realtime-notifications/) | WebSocket + SSE real-time notifications |
|
|
158
|
+
| [`examples/microservices/`](examples/microservices/) | Multi-language microservices |
|
|
159
|
+
| [`examples/fast-start-demo/`](examples/fast-start-demo/) | **NEW:** Fast startup with dependency caching |
|
|
160
|
+
| [`examples/security-policy/`](examples/security-policy/) | **NEW:** Rate limiting and user profiles |
|
|
161
|
+
| [`examples/user-isolation/`](examples/user-isolation/) | **NEW:** Multi-tenant user isolation |
|
|
106
162
|
|
|
107
163
|
## Installation
|
|
108
164
|
|
|
@@ -145,31 +201,31 @@ services:
|
|
|
145
201
|
|
|
146
202
|
Each service is a standard markpact README:
|
|
147
203
|
|
|
148
|
-
|
|
204
|
+
````markdown
|
|
149
205
|
# API Service
|
|
150
206
|
|
|
151
207
|
REST API for the application.
|
|
152
208
|
|
|
153
209
|
---
|
|
154
210
|
|
|
155
|
-
|
|
211
|
+
```python markpact:deps
|
|
156
212
|
fastapi
|
|
157
213
|
uvicorn
|
|
158
|
-
|
|
214
|
+
```
|
|
159
215
|
|
|
160
|
-
|
|
216
|
+
```python markpact:file path=main.py
|
|
161
217
|
from fastapi import FastAPI
|
|
162
218
|
app = FastAPI()
|
|
163
219
|
|
|
164
220
|
@app.get("/health")
|
|
165
221
|
def health():
|
|
166
222
|
return {"status": "ok"}
|
|
167
|
-
|
|
223
|
+
```
|
|
168
224
|
|
|
169
|
-
|
|
225
|
+
```bash markpact:run
|
|
170
226
|
uvicorn main:app --port ${MARKPACT_PORT:-8001}
|
|
171
|
-
\`\`\`
|
|
172
227
|
```
|
|
228
|
+
````
|
|
173
229
|
|
|
174
230
|
### 3. Start the ecosystem
|
|
175
231
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
pactown/__init__.py,sha256=mmmdTjBpjrS1yjksg5JEQEbQhwQhXa-v_FkCqqu5xrI,4417
|
|
2
|
+
pactown/cli.py,sha256=EoQxC5A82-kz72jO97SbPzaeDvjumpIUFyRzhbSK7jU,29731
|
|
3
|
+
pactown/config.py,sha256=gDPa1Mp721W61fWxZexQTOM2-vFESoeeWGaei9SacdE,5333
|
|
4
|
+
pactown/events.py,sha256=BRjUY8a9r4WKedqZK1LzC9ln3dFUnlBRVEa9kqUnSrA,36955
|
|
5
|
+
pactown/fast_start.py,sha256=077Z789IJwOt2OcDl4LDQdCz9xuvZmLNKBFpstg3j-E,17141
|
|
6
|
+
pactown/generator.py,sha256=Sqjn0t-D23jT-BMsMQ6x0V3jnkwf0NtU_wDkLUuUXi4,5805
|
|
7
|
+
pactown/llm.py,sha256=1iwLGxX8ShxqBBRt0t2ILjH3RokADpXx2un317jraDo,14852
|
|
8
|
+
pactown/markpact_blocks.py,sha256=C3ew5KKGYLsi4LFZ_FMotDKzFLfGJ70qPR8h3d3uxAw,1402
|
|
9
|
+
pactown/network.py,sha256=7u6Rt8ZvCXppAE0ONKTrq0j60T1dZvgZP3LCxrMk-so,8577
|
|
10
|
+
pactown/orchestrator.py,sha256=hZ4tmdcD2nTLG9HIUu_t5NhJId7-u8Cx64lGfIrEZrk,15888
|
|
11
|
+
pactown/parallel.py,sha256=vz1MQqZNH6bQN803ch48Xc5zwaMrXTZrPhLu4cEAU14,8095
|
|
12
|
+
pactown/platform.py,sha256=YpGA5Fb0_lIh4FKKlHs63GNbcp3cA-doJmG1FIGJypY,4442
|
|
13
|
+
pactown/resolver.py,sha256=mR6gqDRuM-MXlzcuqK_GF8i9HuOj8uNS9ZoJxLSHs30,5615
|
|
14
|
+
pactown/runner_api.py,sha256=V6eomMxQUyhU4I5W4p4t9EMph4aYHn3rhxend0M9v9s,17625
|
|
15
|
+
pactown/sandbox_manager.py,sha256=joB6Y20cyLfgXlJEw2yElR_6UoszoRTroo1lTD6hkQc,26795
|
|
16
|
+
pactown/security.py,sha256=XlrzFUd85W9xTLK-KVPgiXpYknGUFe48hyNi6ZY_EqM,24945
|
|
17
|
+
pactown/service_runner.py,sha256=NR9Ydx8qZkNLDyhc46s3lFPViriHDMe2F1KC5if4O4E,45067
|
|
18
|
+
pactown/user_isolation.py,sha256=MMa7-maCdr7kSpEvIpaHrD8v54Vf_d6f1Ywv8FtJV2E,16645
|
|
19
|
+
pactown/deploy/__init__.py,sha256=N0cBpIShIWTOqn9XOWbqdDy6poFsnyIEPHUyTtB76TM,813
|
|
20
|
+
pactown/deploy/base.py,sha256=wuO2HgOc8BQibbqQcEqYF3YuopMiUULf0qnJfxzPWec,7740
|
|
21
|
+
pactown/deploy/compose.py,sha256=O7_M6Nc6Nniw70CoQ_9OIOEZpAEuTHABk85HPaL0QzE,11044
|
|
22
|
+
pactown/deploy/docker.py,sha256=Y6C4wW89t-qyYZetR2PDF6KLq3KWyzMmJRexzOBrJ7k,9315
|
|
23
|
+
pactown/deploy/kubernetes.py,sha256=S3hDrAEOvOJgu4qQ7TizKBD46M9DBuWocmlUdF49iKY,14567
|
|
24
|
+
pactown/deploy/podman.py,sha256=Zdu5aLLecdGVKsyF0GZAxRddNnNyqfABgKvBhCaUKnk,12152
|
|
25
|
+
pactown/deploy/quadlet.py,sha256=BcELmjhXmChKDIO0odnMO7prJOrPzDX-0xOb91_-IvQ,29505
|
|
26
|
+
pactown/deploy/quadlet_api.py,sha256=kM6A79LFG-FhG6w5iuP-wV0fcPbWx4kGhTJJu_5_MI8,17489
|
|
27
|
+
pactown/deploy/quadlet_shell.py,sha256=sYZeuz_W1RL00Ncg3sDDI5-E-h9Uo78m3rdYJ0KZcO8,18720
|
|
28
|
+
pactown/registry/__init__.py,sha256=1nv3JsKStu1aPl9Jy6pEJTFg2yk6srayNIas4KjfSKs,278
|
|
29
|
+
pactown/registry/client.py,sha256=kgIp9zQ_Jh_GxGAEgrsVnDNz5mYsmcf-8NvsOU9f0Ik,7750
|
|
30
|
+
pactown/registry/models.py,sha256=CpUUORDAJbDbLU5KZ3F0MjluX8EAZRYOS4xINFPEf9g,5316
|
|
31
|
+
pactown/registry/server.py,sha256=tnwrX6AXDDHHaxiW2bX3N_FqbbZUSFgTYZ1cB43ggp0,6161
|
|
32
|
+
pactown-0.1.47.dist-info/METADATA,sha256=N5yma7rPhyOh9sKeoNza1kDv5hKsvRCWXKWVU1elm0Y,15503
|
|
33
|
+
pactown-0.1.47.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
pactown-0.1.47.dist-info/entry_points.txt,sha256=UH1jCyqsE6rPgxYvU1ZKOFlWZCNK0yi953y44r8W-VE,195
|
|
35
|
+
pactown-0.1.47.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
36
|
+
pactown-0.1.47.dist-info/RECORD,,
|
pactown-0.1.4.dist-info/RECORD
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
pactown/__init__.py,sha256=w9Wv0Ssx_yWabMDy1JJh9oa4Vk7Uj_gDQvdo3cOXbeo,640
|
|
2
|
-
pactown/cli.py,sha256=P7h2-iigtV5JHN6xX9vdKd0nmbr8GFNdXavKYwiUbHs,12431
|
|
3
|
-
pactown/config.py,sha256=4i0A_9AtRCy3khV4jSgIos5YnVdkALWpL27NlbKpkNA,5393
|
|
4
|
-
pactown/generator.py,sha256=TBC3ElDNXo-vDj6n4nDGRynVNo1rdfeMp1hYzHjfOkI,5936
|
|
5
|
-
pactown/network.py,sha256=JRr6zcn2jsr2c-rIDh5mTutPau28Y-JkROS5VeHB1D4,8013
|
|
6
|
-
pactown/orchestrator.py,sha256=um2EcdNBT7UYG16T2MO-JVaDPGn3uJ67UkofLIHDy8Q,16610
|
|
7
|
-
pactown/parallel.py,sha256=GpnAijFFXcqvz08QgUEpJdDoeRMQ-Fq_VxrbRmyNeqo,8362
|
|
8
|
-
pactown/resolver.py,sha256=CXss7EgjkMk2J_6wUP_Dx6m4BYTbFD7dMhMfXwpf1Do,5839
|
|
9
|
-
pactown/sandbox_manager.py,sha256=SrAsF6E6bM2-javMhSoLwKHiOtq6VnD3tIa7Nwlktkc,11086
|
|
10
|
-
pactown/deploy/__init__.py,sha256=GP-Bon4XtUsvNpvVShlJ7cxV8leOv2Pk2bhcHxMpPBs,473
|
|
11
|
-
pactown/deploy/base.py,sha256=8rIyq4wl1QSXeQqWA9DTvr9d70dae2WhA4BXHtgm8fg,7535
|
|
12
|
-
pactown/deploy/compose.py,sha256=eBDn_8QPUpnBQOw6S7fA-xS-bzYAEd9XKuKWDwz2kQ4,11427
|
|
13
|
-
pactown/deploy/docker.py,sha256=5MQOdH78KnyrNRq4sCPNIA8cjifU8uHT7gbkQA-zosA,9633
|
|
14
|
-
pactown/deploy/kubernetes.py,sha256=1z5HVhGc1K-JoNpIp2mu5OU3ufz8Fx4BH2clQjutGWI,14826
|
|
15
|
-
pactown/deploy/podman.py,sha256=U8cTSTKfiHSCBbQvqhicYXO3XJpdu4f2kURo62Xp2sA,12574
|
|
16
|
-
pactown/registry/__init__.py,sha256=_OPop2RWt8cjdsV6EYCIXEkuxS4PjWm72gMiwUkCE-U,278
|
|
17
|
-
pactown/registry/client.py,sha256=EoD8PTUPmJeqSe1OLuFO8M_XbEyFox2L9E3oKO2SAy4,7933
|
|
18
|
-
pactown/registry/models.py,sha256=wlL9Jsl60Gf27pUe8ej-oWSxRBlFOReLsr9W7joKiPI,5293
|
|
19
|
-
pactown/registry/server.py,sha256=chLI1EUhlayeoaJzMnmHyjvsSU0y2JKwOLJnle0DyuQ,6303
|
|
20
|
-
pactown-0.1.4.dist-info/METADATA,sha256=tXziMYrGKzF8_DXr31aZqfUOpH4g_7zqoJPe39tgDXA,11891
|
|
21
|
-
pactown-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
22
|
-
pactown-0.1.4.dist-info/entry_points.txt,sha256=v8Wxhuh8J_-OT6Y5FjE48dzbRGXUiznz0kUxSk5gtM0,93
|
|
23
|
-
pactown-0.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
24
|
-
pactown-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|