portacode 1.4.13.dev1__py3-none-any.whl → 1.4.13.dev2__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.
portacode/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.4.13.dev1'
32
- __version_tuple__ = version_tuple = (1, 4, 13, 'dev1')
31
+ __version__ = version = '1.4.13.dev2'
32
+ __version_tuple__ = version_tuple = (1, 4, 13, 'dev2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -27,8 +27,6 @@ The Portacode server acts as a **routing middleman** between client sessions and
27
27
 
28
28
  - **`source_client_session`** (Server → Device): Server **adds this** when forwarding client commands to devices (so device knows which client sent the command and can target responses back). Clients never include this field.
29
29
 
30
- Device handlers that want to respond only to the session that issued a given command can rely on the helper defined in `portacode/connection/handlers/base.py` (`BaseHandler.send_response_to_source_session`). It wraps the boilerplate of reusing `request_id`/`trace`, injecting `client_sessions=[source_client_session]`, and sending via the control channel so the handler does not need to manually reconstruct routing metadata for each reply.
31
-
32
30
  This document describes the complete protocol for communicating with devices through the server, guiding app developers on how to get their client sessions to communicate with devices.
33
31
 
34
32
  ## Table of Contents
@@ -363,6 +361,9 @@ Creates a Portacode-managed LXC container, starts it, and bootstraps the Portaco
363
361
  * `username` (string, optional): OS user to provision (defaults to `svcuser`).
364
362
  * `password` (string, optional): Password for the user (used only during provisioning).
365
363
  * `ssh_key` (string, optional): SSH public key to add to the user.
364
+ * `device_id` (string, optional): ID of the Device record that already exists on the dashboard.
365
+ * `device_public_key` (string, optional): PEM-encoded Portacode public key. When supplied together with `device_private_key` the handler injects the keypair, records the device metadata, and runs `portacode service install` automatically.
366
+ * `device_private_key` (string, optional): PEM-encoded private key that pairs with `device_public_key`. Both key fields must be present for the automatic service-install mode.
366
367
 
367
368
  **Responses:**
368
369
 
@@ -420,6 +421,8 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
420
421
  * `public_key` (string): Portacode public auth key created inside the new container.
421
422
  * `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
422
423
  * `setup_steps` (array[object]): Detailed bootstrap step results (name, stdout/stderr, elapsed time, and status).
424
+ * `device_id` (string, optional): Mirrors the `device_id` supplied with `create_proxmox_container`, if any.
425
+ * `service_installed` (boolean): True when the handler already ran `portacode service install` (with a provided keypair); otherwise it remains False and the dashboard can call `start_portacode_service`.
423
426
 
424
427
  ### `proxmox_container_progress`
425
428
 
@@ -452,6 +455,7 @@ Runs `sudo portacode service install` inside the container after the dashboard h
452
455
  * Emits additional [`proxmox_container_progress`](#proxmox_container_progress-event) events to report the authentication and service-install steps.
453
456
  * On success, emits a [`proxmox_service_started`](#proxmox_service_started-event).
454
457
  * On failure, emits a generic [`error`](#error) event.
458
+ * When `create_proxmox_container` already provided a dashboard-generated keypair, the handler may have installed the service already, so this call is optional unless you need to re-run the install.
455
459
 
456
460
  ### `proxmox_service_started`
457
461
 
@@ -88,42 +88,6 @@ class BaseHandler(ABC):
88
88
  if reply_channel:
89
89
  payload["reply_channel"] = reply_channel
90
90
  await self.control_channel.send(payload)
91
-
92
- async def send_response_to_source_session(
93
- self,
94
- message: Dict[str, Any],
95
- payload: Dict[str, Any],
96
- reply_channel: Optional[str] = None,
97
- ) -> None:
98
- """Send a response directly to the client session that issued ``message``.
99
-
100
- This bypasses ``ClientSessionManager`` routing and ensures only the session
101
- whose ``source_client_session`` was injected by the server will receive the
102
- response. The helper also copies ``request_id``/``trace`` timing data so tracing
103
- and logging remain consistent.
104
- """
105
- response = dict(payload)
106
- request_id = message.get("request_id")
107
- if request_id and "request_id" not in response:
108
- response["request_id"] = request_id
109
-
110
- if "trace" in message and "request_id" in message and "trace" not in response:
111
- trace_data = dict(message["trace"])
112
- handler_complete_time = ntp_clock.now_ms()
113
- if handler_complete_time is not None:
114
- trace_data["handler_complete"] = handler_complete_time
115
- if "client_send" in trace_data:
116
- trace_data["ping"] = handler_complete_time - trace_data["client_send"]
117
- response["trace"] = trace_data
118
-
119
- source_client_session = message.get("source_client_session")
120
- if source_client_session:
121
- response["client_sessions"] = [source_client_session]
122
-
123
- if reply_channel:
124
- response["reply_channel"] = reply_channel
125
-
126
- await self.control_channel.send(response)
127
91
 
128
92
  async def send_error(self, message: str, reply_channel: Optional[str] = None, project_id: str = None) -> None:
129
93
  """Send an error response with client session awareness.
@@ -177,16 +141,22 @@ class AsyncHandler(BaseHandler):
177
141
  if "request_id" in message and "request_id" not in response:
178
142
  response["request_id"] = message["request_id"]
179
143
 
180
- if "trace" in message and "request_id" in message and "trace" not in response:
181
- trace_data = dict(message["trace"])
144
+ # Pass through trace from request to response (add to existing trace, don't create new one)
145
+ if "trace" in message and "request_id" in message:
146
+ response["trace"] = dict(message["trace"])
182
147
  handler_complete_time = ntp_clock.now_ms()
183
148
  if handler_complete_time is not None:
184
- trace_data["handler_complete"] = handler_complete_time
185
- if "client_send" in trace_data:
186
- trace_data["ping"] = handler_complete_time - trace_data["client_send"]
187
- response["trace"] = trace_data
188
- logger.info(f"✅ Handler completed: {message['request_id']} ({self.command_name})")
189
-
149
+ response["trace"]["handler_complete"] = handler_complete_time
150
+ # Update ping to show total time from client_send
151
+ if "client_send" in response["trace"]:
152
+ response["trace"]["ping"] = handler_complete_time - response["trace"]["client_send"]
153
+ logger.info(f"✅ Handler completed: {message['request_id']} ({self.command_name})")
154
+
155
+ # Extract project_id from response for session targeting
156
+ project_id = response.get("project_id")
157
+ logger.info("handler: %s response project_id=%s, response=%s",
158
+ self.command_name, project_id, response)
159
+ await self.send_response(response, reply_channel, project_id)
190
160
  else:
191
161
  logger.info("handler: %s handled response transmission directly", self.command_name)
192
162
  except Exception as exc:
@@ -246,4 +216,4 @@ class SyncHandler(BaseHandler):
246
216
  logger.exception("Error in sync handler %s: %s", self.command_name, exc)
247
217
  # Extract project_id from original message for error targeting
248
218
  project_id = message.get("project_id")
249
- await self.send_error(str(exc), reply_channel, project_id)
219
+ await self.send_error(str(exc), reply_channel, project_id)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import base64
6
7
  import json
7
8
  import logging
8
9
  import os
@@ -11,7 +12,6 @@ import shutil
11
12
  import stat
12
13
  import subprocess
13
14
  import sys
14
- import time
15
15
  import threading
16
16
  from datetime import datetime
17
17
  from pathlib import Path
@@ -59,7 +59,6 @@ def _emit_progress_event(
59
59
  message: str,
60
60
  phase: str,
61
61
  request_id: Optional[str],
62
- client_sessions: Optional[List[str]] = None,
63
62
  details: Optional[Dict[str, Any]] = None,
64
63
  ) -> None:
65
64
  loop = handler.context.get("event_loop")
@@ -85,8 +84,6 @@ def _emit_progress_event(
85
84
  payload["request_id"] = request_id
86
85
  if details:
87
86
  payload["details"] = details
88
- if client_sessions:
89
- payload["client_sessions"] = client_sessions
90
87
 
91
88
  future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
92
89
  future.add_done_callback(
@@ -431,7 +428,12 @@ def _friendly_step_label(step_name: str) -> str:
431
428
  return normalized.capitalize()
432
429
 
433
430
 
434
- def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[str, Any]]:
431
+ def _build_bootstrap_steps(
432
+ user: str,
433
+ password: str,
434
+ ssh_key: str,
435
+ include_portacode_connect: bool = True,
436
+ ) -> List[Dict[str, Any]]:
435
437
  steps = [
436
438
  {
437
439
  "name": "apt_update",
@@ -468,11 +470,14 @@ def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[
468
470
  "cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
469
471
  "retries": 0,
470
472
  })
471
- steps.extend([
472
- {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
473
- {"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
474
- {"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
475
- ])
473
+ steps.extend(
474
+ [
475
+ {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
476
+ {"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
477
+ ]
478
+ )
479
+ if include_portacode_connect:
480
+ steps.append({"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30})
476
481
  return steps
477
482
 
478
483
 
@@ -682,6 +687,36 @@ def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
682
687
  return res
683
688
 
684
689
 
690
+ def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
691
+ data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
692
+ data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
693
+ return f"{data_home}/portacode/keys"
694
+
695
+
696
+ def _write_bytes_as_user(vmid: int, user: str, path: str, data: bytes, mode: int = 0o600) -> None:
697
+ encoded = base64.b64encode(data).decode()
698
+ path_literal = json.dumps(path)
699
+ script = (
700
+ f"su - {user} -c 'python3 - <<\"PY\"\n"
701
+ "import base64\n"
702
+ "from pathlib import Path\n"
703
+ f"path = Path({path_literal})\n"
704
+ "path.parent.mkdir(parents=True, exist_ok=True)\n"
705
+ f"path.write_bytes(base64.b64decode(\"{encoded}\"))\n"
706
+ f"path.chmod({mode})\n"
707
+ "PY'"
708
+ )
709
+ _run_pct_check(vmid, script)
710
+
711
+
712
+ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: str) -> None:
713
+ key_dir = _resolve_portacode_key_dir(vmid, user)
714
+ priv_path = f"{key_dir}/id_portacode"
715
+ pub_path = f"{key_dir}/id_portacode.pub"
716
+ _write_bytes_as_user(vmid, user, priv_path, private_key.encode(), mode=0o600)
717
+ _write_bytes_as_user(vmid, user, pub_path, public_key.encode(), mode=0o644)
718
+
719
+
685
720
  def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
686
721
  cmd = ["pct", "exec", str(vmid), "--", "bash", "-lc", f"su - {user} -c 'portacode connect'"]
687
722
  proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -883,6 +918,7 @@ def _bootstrap_portacode(
883
918
  progress_callback: Optional[ProgressCallback] = None,
884
919
  start_index: int = 1,
885
920
  total_steps: Optional[int] = None,
921
+ default_public_key: Optional[str] = None,
886
922
  ) -> Tuple[str, List[Dict[str, Any]]]:
887
923
  actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
888
924
  results, ok = _run_setup_steps(
@@ -909,7 +945,7 @@ def _bootstrap_portacode(
909
945
  raise RuntimeError(f"Portacode bootstrap steps failed: {summary}{history_snippet}")
910
946
  raise RuntimeError("Portacode bootstrap steps failed.")
911
947
  key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
912
- public_key = key_step.get("public_key") if key_step else None
948
+ public_key = key_step.get("public_key") if key_step else default_public_key
913
949
  if not public_key:
914
950
  raise RuntimeError("Portacode connect did not return a public key.")
915
951
  return public_key, results
@@ -1053,16 +1089,19 @@ class CreateProxmoxContainerHandler(SyncHandler):
1053
1089
  return "create_proxmox_container"
1054
1090
 
1055
1091
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1092
+ logger.info("create_proxmox_container command received")
1056
1093
  request_id = message.get("request_id")
1057
- source_client_session = message.get("source_client_session")
1058
- logger.info(
1059
- "create_proxmox_container command received request_id=%s client_session=%s",
1060
- request_id,
1061
- source_client_session,
1062
- )
1063
- client_sessions = [source_client_session] if source_client_session else None
1094
+ device_id = message.get("device_id")
1095
+ device_public_key = (message.get("device_public_key") or "").strip()
1096
+ device_private_key = (message.get("device_private_key") or "").strip()
1097
+ has_device_keypair = bool(device_public_key and device_private_key)
1064
1098
  bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
1065
- bootstrap_steps = _build_bootstrap_steps(bootstrap_user, bootstrap_password, bootstrap_ssh_key)
1099
+ bootstrap_steps = _build_bootstrap_steps(
1100
+ bootstrap_user,
1101
+ bootstrap_password,
1102
+ bootstrap_ssh_key,
1103
+ include_portacode_connect=not has_device_keypair,
1104
+ )
1066
1105
  total_steps = 3 + len(bootstrap_steps) + 2
1067
1106
  current_step_index = 1
1068
1107
 
@@ -1084,7 +1123,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1084
1123
  message=start_message,
1085
1124
  phase="lifecycle",
1086
1125
  request_id=request_id,
1087
- client_sessions=client_sessions,
1088
1126
  )
1089
1127
  try:
1090
1128
  result = action()
@@ -1099,7 +1137,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1099
1137
  message=f"{step_label} failed: {exc}",
1100
1138
  phase="lifecycle",
1101
1139
  request_id=request_id,
1102
- client_sessions=client_sessions,
1103
1140
  details={"error": str(exc)},
1104
1141
  )
1105
1142
  raise
@@ -1113,7 +1150,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1113
1150
  message=success_message,
1114
1151
  phase="lifecycle",
1115
1152
  request_id=request_id,
1116
- client_sessions=client_sessions,
1117
1153
  )
1118
1154
  current_step_index += 1
1119
1155
  return result
@@ -1143,7 +1179,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
1143
1179
  payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
1144
1180
  payload["memory"] = int(payload["ram_mib"])
1145
1181
  payload["node"] = node
1146
- logger.info(
1182
+ logger.debug(
1147
1183
  "Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
1148
1184
  node,
1149
1185
  payload["template"],
@@ -1156,12 +1192,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1156
1192
  payload["created_at"] = datetime.utcnow().isoformat() + "Z"
1157
1193
  payload["status"] = "creating"
1158
1194
  _write_container_record(vmid, payload)
1159
- logger.info(
1160
- "Container record written vmid=%s hostname=%s client_session=%s",
1161
- vmid,
1162
- payload["hostname"],
1163
- source_client_session,
1164
- )
1165
1195
  return proxmox, node, vmid, payload
1166
1196
 
1167
1197
  proxmox, node, vmid, payload = _run_lifecycle_step(
@@ -1220,16 +1250,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
1220
1250
  message=message_text,
1221
1251
  phase="bootstrap",
1222
1252
  request_id=request_id,
1223
- client_sessions=client_sessions,
1224
1253
  details=details or None,
1225
1254
  )
1226
1255
 
1227
- logger.info(
1228
- "Bootstrapping Portacode in container vmid=%s user=%s client_session=%s",
1229
- vmid,
1230
- payload["username"],
1231
- source_client_session,
1232
- )
1233
1256
  public_key, steps = _bootstrap_portacode(
1234
1257
  vmid,
1235
1258
  payload["username"],
@@ -1239,10 +1262,103 @@ class CreateProxmoxContainerHandler(SyncHandler):
1239
1262
  progress_callback=_bootstrap_progress_callback,
1240
1263
  start_index=current_step_index,
1241
1264
  total_steps=total_steps,
1265
+ default_public_key=device_public_key if has_device_keypair else None,
1242
1266
  )
1243
1267
  current_step_index += len(bootstrap_steps)
1244
1268
 
1245
- response = {
1269
+ service_installed = False
1270
+ if has_device_keypair:
1271
+ logger.info(
1272
+ "deploying dashboard-provided Portacode keypair (device_id=%s) into container %s",
1273
+ device_id,
1274
+ vmid,
1275
+ )
1276
+ _deploy_device_keypair(
1277
+ vmid,
1278
+ payload["username"],
1279
+ device_private_key,
1280
+ device_public_key,
1281
+ )
1282
+ service_installed = True
1283
+ service_start_index = current_step_index
1284
+
1285
+ auth_step_name = "setup_device_authentication"
1286
+ auth_label = "Setting up device authentication"
1287
+ _emit_progress_event(
1288
+ self,
1289
+ step_index=service_start_index,
1290
+ total_steps=total_steps,
1291
+ step_name=auth_step_name,
1292
+ step_label=auth_label,
1293
+ status="in_progress",
1294
+ message="Notifying the server of the new device…",
1295
+ phase="service",
1296
+ request_id=request_id,
1297
+ )
1298
+ _emit_progress_event(
1299
+ self,
1300
+ step_index=service_start_index,
1301
+ total_steps=total_steps,
1302
+ step_name=auth_step_name,
1303
+ step_label=auth_label,
1304
+ status="completed",
1305
+ message="Authentication metadata recorded.",
1306
+ phase="service",
1307
+ request_id=request_id,
1308
+ )
1309
+
1310
+ install_step = service_start_index + 1
1311
+ install_label = "Launching Portacode service"
1312
+ _emit_progress_event(
1313
+ self,
1314
+ step_index=install_step,
1315
+ total_steps=total_steps,
1316
+ step_name="launch_portacode_service",
1317
+ step_label=install_label,
1318
+ status="in_progress",
1319
+ message="Running sudo portacode service install…",
1320
+ phase="service",
1321
+ request_id=request_id,
1322
+ )
1323
+
1324
+ cmd = f"su - {payload['username']} -c 'sudo -S portacode service install'"
1325
+ res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
1326
+
1327
+ if res["returncode"] != 0:
1328
+ _emit_progress_event(
1329
+ self,
1330
+ step_index=install_step,
1331
+ total_steps=total_steps,
1332
+ step_name="launch_portacode_service",
1333
+ step_label=install_label,
1334
+ status="failed",
1335
+ message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
1336
+ phase="service",
1337
+ request_id=request_id,
1338
+ details={
1339
+ "stderr": res.get("stderr"),
1340
+ "stdout": res.get("stdout"),
1341
+ },
1342
+ )
1343
+ raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
1344
+
1345
+ _emit_progress_event(
1346
+ self,
1347
+ step_index=install_step,
1348
+ total_steps=total_steps,
1349
+ step_name="launch_portacode_service",
1350
+ step_label=install_label,
1351
+ status="completed",
1352
+ message="Portacode service install finished.",
1353
+ phase="service",
1354
+ request_id=request_id,
1355
+ )
1356
+
1357
+ logger.info("create_proxmox_container: portacode service install completed inside ct %s", vmid)
1358
+
1359
+ current_step_index += 2
1360
+
1361
+ return {
1246
1362
  "event": "proxmox_container_created",
1247
1363
  "success": True,
1248
1364
  "message": f"Container {vmid} is ready and Portacode key captured.",
@@ -1258,11 +1374,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
1258
1374
  "cpus": payload["cpus"],
1259
1375
  },
1260
1376
  "setup_steps": steps,
1377
+ "device_id": device_id,
1378
+ "service_installed": service_installed,
1261
1379
  }
1262
- if client_sessions:
1263
- response["client_sessions"] = client_sessions
1264
- response["_reply_to_source_session"] = True
1265
- return response
1266
1380
 
1267
1381
 
1268
1382
  class StartPortacodeServiceHandler(SyncHandler):
@@ -1290,15 +1404,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1290
1404
  start_index = int(message.get("step_index", 1))
1291
1405
  total_steps = int(message.get("total_steps", start_index + 2))
1292
1406
  request_id = message.get("request_id")
1293
- source_client_session = message.get("source_client_session")
1294
- client_sessions = [source_client_session] if source_client_session else None
1295
-
1296
- logger.info(
1297
- "start_portacode_service invoked vmid=%s user=%s client_session=%s",
1298
- vmid,
1299
- user,
1300
- source_client_session,
1301
- )
1302
1407
 
1303
1408
  auth_step_name = "setup_device_authentication"
1304
1409
  auth_label = "Setting up device authentication"
@@ -1312,7 +1417,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1312
1417
  message="Notifying the server of the new device…",
1313
1418
  phase="service",
1314
1419
  request_id=request_id,
1315
- client_sessions=client_sessions,
1316
1420
  )
1317
1421
  _emit_progress_event(
1318
1422
  self,
@@ -1324,7 +1428,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1324
1428
  message="Authentication metadata recorded.",
1325
1429
  phase="service",
1326
1430
  request_id=request_id,
1327
- client_sessions=client_sessions,
1328
1431
  )
1329
1432
 
1330
1433
  install_step = start_index + 1
@@ -1339,7 +1442,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1339
1442
  message="Running sudo portacode service install…",
1340
1443
  phase="service",
1341
1444
  request_id=request_id,
1342
- client_sessions=client_sessions,
1343
1445
  )
1344
1446
 
1345
1447
  cmd = f"su - {user} -c 'sudo -S portacode service install'"
@@ -1356,7 +1458,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1356
1458
  message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
1357
1459
  phase="service",
1358
1460
  request_id=request_id,
1359
- client_sessions=client_sessions,
1360
1461
  details={
1361
1462
  "stderr": res.get("stderr"),
1362
1463
  "stdout": res.get("stdout"),
@@ -1364,12 +1465,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1364
1465
  )
1365
1466
  raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
1366
1467
 
1367
- logger.info(
1368
- "portacode service install command completed vmid=%s returncode=%s",
1369
- vmid,
1370
- res["returncode"],
1371
- )
1372
-
1373
1468
  _emit_progress_event(
1374
1469
  self,
1375
1470
  step_index=install_step,
@@ -1380,19 +1475,14 @@ class StartPortacodeServiceHandler(SyncHandler):
1380
1475
  message="Portacode service install finished.",
1381
1476
  phase="service",
1382
1477
  request_id=request_id,
1383
- client_sessions=client_sessions,
1384
1478
  )
1385
1479
 
1386
- response = {
1480
+ return {
1387
1481
  "event": "proxmox_service_started",
1388
1482
  "success": True,
1389
1483
  "message": "Portacode service install completed",
1390
1484
  "ctid": str(vmid),
1391
1485
  }
1392
- if client_sessions:
1393
- response["client_sessions"] = client_sessions
1394
- response["_reply_to_source_session"] = True
1395
- return response
1396
1486
 
1397
1487
 
1398
1488
  class StartProxmoxContainerHandler(SyncHandler):
@@ -1408,20 +1498,12 @@ class StartProxmoxContainerHandler(SyncHandler):
1408
1498
  proxmox = _connect_proxmox(config)
1409
1499
  node = _get_node_from_config(config)
1410
1500
  _ensure_container_managed(proxmox, node, vmid)
1411
- source_client_session = message.get("source_client_session")
1412
- client_sessions = [source_client_session] if source_client_session else None
1413
-
1414
- logger.info(
1415
- "start_proxmox_container invoked vmid=%s client_session=%s",
1416
- vmid,
1417
- source_client_session,
1418
- )
1419
1501
 
1420
1502
  status, elapsed = _start_container(proxmox, node, vmid)
1421
1503
  _update_container_record(vmid, {"status": "running"})
1422
1504
 
1423
1505
  infra = get_infra_snapshot()
1424
- response = {
1506
+ return {
1425
1507
  "event": "proxmox_container_action",
1426
1508
  "action": "start",
1427
1509
  "success": True,
@@ -1431,10 +1513,6 @@ class StartProxmoxContainerHandler(SyncHandler):
1431
1513
  "status": status.get("status"),
1432
1514
  "infra": infra,
1433
1515
  }
1434
- if client_sessions:
1435
- response["client_sessions"] = client_sessions
1436
- response["_reply_to_source_session"] = True
1437
- return response
1438
1516
 
1439
1517
 
1440
1518
  class StopProxmoxContainerHandler(SyncHandler):
@@ -1450,14 +1528,6 @@ class StopProxmoxContainerHandler(SyncHandler):
1450
1528
  proxmox = _connect_proxmox(config)
1451
1529
  node = _get_node_from_config(config)
1452
1530
  _ensure_container_managed(proxmox, node, vmid)
1453
- source_client_session = message.get("source_client_session")
1454
- client_sessions = [source_client_session] if source_client_session else None
1455
-
1456
- logger.info(
1457
- "stop_proxmox_container invoked vmid=%s client_session=%s",
1458
- vmid,
1459
- source_client_session,
1460
- )
1461
1531
 
1462
1532
  status, elapsed = _stop_container(proxmox, node, vmid)
1463
1533
  final_status = status.get("status") or "stopped"
@@ -1469,7 +1539,7 @@ class StopProxmoxContainerHandler(SyncHandler):
1469
1539
  if final_status != "running" and elapsed == 0.0
1470
1540
  else f"Stopped container {vmid} in {elapsed:.1f}s."
1471
1541
  )
1472
- response = {
1542
+ return {
1473
1543
  "event": "proxmox_container_action",
1474
1544
  "action": "stop",
1475
1545
  "success": True,
@@ -1479,10 +1549,6 @@ class StopProxmoxContainerHandler(SyncHandler):
1479
1549
  "status": final_status,
1480
1550
  "infra": infra,
1481
1551
  }
1482
- if client_sessions:
1483
- response["client_sessions"] = client_sessions
1484
- response["_reply_to_source_session"] = True
1485
- return response
1486
1552
 
1487
1553
 
1488
1554
  class RemoveProxmoxContainerHandler(SyncHandler):
@@ -1498,21 +1564,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1498
1564
  proxmox = _connect_proxmox(config)
1499
1565
  node = _get_node_from_config(config)
1500
1566
  _ensure_container_managed(proxmox, node, vmid)
1501
- source_client_session = message.get("source_client_session")
1502
- client_sessions = [source_client_session] if source_client_session else None
1503
-
1504
- logger.info(
1505
- "remove_proxmox_container invoked vmid=%s client_session=%s",
1506
- vmid,
1507
- source_client_session,
1508
- )
1509
1567
 
1510
1568
  stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
1511
1569
  delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
1512
1570
  _remove_container_record(vmid)
1513
1571
 
1514
1572
  infra = get_infra_snapshot()
1515
- response = {
1573
+ return {
1516
1574
  "event": "proxmox_container_action",
1517
1575
  "action": "remove",
1518
1576
  "success": True,
@@ -1525,10 +1583,6 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1525
1583
  "status": "deleted",
1526
1584
  "infra": infra,
1527
1585
  }
1528
- if client_sessions:
1529
- response["client_sessions"] = client_sessions
1530
- response["_reply_to_source_session"] = True
1531
- return response
1532
1586
 
1533
1587
 
1534
1588
  class ConfigureProxmoxInfraHandler(SyncHandler):
@@ -1542,19 +1596,13 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
1542
1596
  verify_ssl = bool(message.get("verify_ssl"))
1543
1597
  if not token_identifier or not token_value:
1544
1598
  raise ValueError("token_identifier and token_value are required")
1545
- source_client_session = message.get("source_client_session")
1546
- client_sessions = [source_client_session] if source_client_session else None
1547
1599
  snapshot = configure_infrastructure(token_identifier, token_value, verify_ssl=verify_ssl)
1548
- response = {
1600
+ return {
1549
1601
  "event": "proxmox_infra_configured",
1550
1602
  "success": True,
1551
1603
  "message": "Proxmox infrastructure configured",
1552
1604
  "infra": snapshot,
1553
1605
  }
1554
- if client_sessions:
1555
- response["client_sessions"] = client_sessions
1556
- response["_reply_to_source_session"] = True
1557
- return response
1558
1606
 
1559
1607
 
1560
1608
  class RevertProxmoxInfraHandler(SyncHandler):
@@ -1563,16 +1611,10 @@ class RevertProxmoxInfraHandler(SyncHandler):
1563
1611
  return "revert_proxmox_infra"
1564
1612
 
1565
1613
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1566
- source_client_session = message.get("source_client_session")
1567
- client_sessions = [source_client_session] if source_client_session else None
1568
1614
  snapshot = revert_infrastructure()
1569
- response = {
1615
+ return {
1570
1616
  "event": "proxmox_infra_reverted",
1571
1617
  "success": True,
1572
1618
  "message": "Proxmox infrastructure configuration reverted",
1573
1619
  "infra": snapshot,
1574
1620
  }
1575
- if client_sessions:
1576
- response["client_sessions"] = client_sessions
1577
- response["_reply_to_source_session"] = True
1578
- return response
@@ -0,0 +1,13 @@
1
+ from unittest import TestCase
2
+
3
+ from portacode.connection.handlers.proxmox_infra import _build_bootstrap_steps
4
+
5
+
6
+ class ProxmoxInfraHandlerTests(TestCase):
7
+ def test_build_bootstrap_steps_includes_portacode_connect_by_default(self):
8
+ steps = _build_bootstrap_steps("svcuser", "pass", "", include_portacode_connect=True)
9
+ self.assertTrue(any(step.get("name") == "portacode_connect" for step in steps))
10
+
11
+ def test_build_bootstrap_steps_skips_portacode_connect_when_requested(self):
12
+ steps = _build_bootstrap_steps("svcuser", "pass", "", include_portacode_connect=False)
13
+ self.assertFalse(any(step.get("name") == "portacode_connect" for step in steps))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.13.dev1
3
+ Version: 1.4.13.dev2
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -1,7 +1,7 @@
1
1
  portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
2
2
  portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
3
3
  portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
4
- portacode/_version.py,sha256=LRfIU5637o003YhvJfKkC-W6_vMYzK36mKyg9SlBZi0,719
4
+ portacode/_version.py,sha256=DNXD9h6WnEeb90CTXVgfmtrpDU4zmIz1R3oJab1QQyY,719
5
5
  portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
6
6
  portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
7
7
  portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
@@ -14,20 +14,21 @@ portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5wei
14
14
  portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
15
15
  portacode/connection/terminal.py,sha256=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
16
16
  portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
17
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=2sFO9VsvZUl57RM6R4cgYzN4WYlIQLASlKMa7r0NMSc,98404
17
+ portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=--NNK-qrqQhZ9O4RHuAQfDQb6QxKtl_No101c1Sy_cQ,98994
18
18
  portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
19
- portacode/connection/handlers/base.py,sha256=YKjXeS_PDbM6RyrSauZBJvmndU2bfvCMOwckYLVLn-U,11296
19
+ portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
20
20
  portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
21
21
  portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZbQw6sF04M3dM6rUV8Q,24477
22
22
  portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
23
23
  portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
24
24
  portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
25
- portacode/connection/handlers/proxmox_infra.py,sha256=bZVlM6k--CyLEBbhPvFafHTN-cyryRxy86EG6PpXoII,57628
25
+ portacode/connection/handlers/proxmox_infra.py,sha256=m9JKbJUBIpuv-7QGU1LBVqoZJXvPQzhNcx1MLatjNhs,58972
26
26
  portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
27
27
  portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
28
28
  portacode/connection/handlers/system_handlers.py,sha256=AKh7IbwptlLYrbSw5f-DHigvlaKHsg9lDP-lkAUm8cE,10755
29
29
  portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
30
30
  portacode/connection/handlers/terminal_handlers.py,sha256=HRwHW1GiqG1NtHVEqXHKaYkFfQEzCDDH6YIlHcb4XD8,11866
31
+ portacode/connection/handlers/test_proxmox_infra.py,sha256=d6iBB4pwAqWWdEGRayLxDEexqCElbGZDJlCB4bXba24,682
31
32
  portacode/connection/handlers/update_handler.py,sha256=f2K4LmG4sHJZ3LahzzoRtHBULTKkPUNwuyhwuAAg3RA,2054
32
33
  portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
33
34
  portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
@@ -64,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
64
65
  portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
65
66
  portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
66
67
  portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
67
- portacode-1.4.13.dev1.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
+ portacode-1.4.13.dev2.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
68
69
  test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
69
70
  test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
70
71
  test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
@@ -90,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
90
91
  testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
91
92
  testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
92
93
  testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
93
- portacode-1.4.13.dev1.dist-info/METADATA,sha256=mSEfLnN_1l9Wjf0LHMvOp5Gai18-BsmIgQ-jD80vAVY,13051
94
- portacode-1.4.13.dev1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
95
- portacode-1.4.13.dev1.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
96
- portacode-1.4.13.dev1.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
97
- portacode-1.4.13.dev1.dist-info/RECORD,,
94
+ portacode-1.4.13.dev2.dist-info/METADATA,sha256=mVp7O5PlhPfvcaUdyshscyW9aH8bfYJ-FNIRkhunycE,13051
95
+ portacode-1.4.13.dev2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
96
+ portacode-1.4.13.dev2.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.13.dev2.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.13.dev2.dist-info/RECORD,,