portacode 1.4.13.dev1__py3-none-any.whl → 1.4.13.dev3__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.dev3'
32
+ __version_tuple__ = version_tuple = (1, 4, 13, 'dev3')
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
@@ -59,7 +60,6 @@ def _emit_progress_event(
59
60
  message: str,
60
61
  phase: str,
61
62
  request_id: Optional[str],
62
- client_sessions: Optional[List[str]] = None,
63
63
  details: Optional[Dict[str, Any]] = None,
64
64
  ) -> None:
65
65
  loop = handler.context.get("event_loop")
@@ -85,8 +85,6 @@ def _emit_progress_event(
85
85
  payload["request_id"] = request_id
86
86
  if details:
87
87
  payload["details"] = details
88
- if client_sessions:
89
- payload["client_sessions"] = client_sessions
90
88
 
91
89
  future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
92
90
  future.add_done_callback(
@@ -431,7 +429,12 @@ def _friendly_step_label(step_name: str) -> str:
431
429
  return normalized.capitalize()
432
430
 
433
431
 
434
- def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[str, Any]]:
432
+ def _build_bootstrap_steps(
433
+ user: str,
434
+ password: str,
435
+ ssh_key: str,
436
+ include_portacode_connect: bool = True,
437
+ ) -> List[Dict[str, Any]]:
435
438
  steps = [
436
439
  {
437
440
  "name": "apt_update",
@@ -468,11 +471,14 @@ def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[
468
471
  "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
472
  "retries": 0,
470
473
  })
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
- ])
474
+ steps.extend(
475
+ [
476
+ {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
477
+ {"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
478
+ ]
479
+ )
480
+ if include_portacode_connect:
481
+ steps.append({"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30})
476
482
  return steps
477
483
 
478
484
 
@@ -682,6 +688,36 @@ def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
682
688
  return res
683
689
 
684
690
 
691
+ def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
692
+ data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
693
+ data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
694
+ return f"{data_home}/portacode/keys"
695
+
696
+
697
+ def _write_bytes_as_user(vmid: int, user: str, path: str, data: bytes, mode: int = 0o600) -> None:
698
+ encoded = base64.b64encode(data).decode()
699
+ path_literal = json.dumps(path)
700
+ script = (
701
+ f"su - {user} -c 'python3 - <<\"PY\"\n"
702
+ "import base64\n"
703
+ "from pathlib import Path\n"
704
+ f"path = Path({path_literal})\n"
705
+ "path.parent.mkdir(parents=True, exist_ok=True)\n"
706
+ f"path.write_bytes(base64.b64decode(\"{encoded}\"))\n"
707
+ f"path.chmod({mode})\n"
708
+ "PY'"
709
+ )
710
+ _run_pct_check(vmid, script)
711
+
712
+
713
+ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: str) -> None:
714
+ key_dir = _resolve_portacode_key_dir(vmid, user)
715
+ priv_path = f"{key_dir}/id_portacode"
716
+ pub_path = f"{key_dir}/id_portacode.pub"
717
+ _write_bytes_as_user(vmid, user, priv_path, private_key.encode(), mode=0o600)
718
+ _write_bytes_as_user(vmid, user, pub_path, public_key.encode(), mode=0o644)
719
+
720
+
685
721
  def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
686
722
  cmd = ["pct", "exec", str(vmid), "--", "bash", "-lc", f"su - {user} -c 'portacode connect'"]
687
723
  proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -883,6 +919,7 @@ def _bootstrap_portacode(
883
919
  progress_callback: Optional[ProgressCallback] = None,
884
920
  start_index: int = 1,
885
921
  total_steps: Optional[int] = None,
922
+ default_public_key: Optional[str] = None,
886
923
  ) -> Tuple[str, List[Dict[str, Any]]]:
887
924
  actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
888
925
  results, ok = _run_setup_steps(
@@ -909,7 +946,7 @@ def _bootstrap_portacode(
909
946
  raise RuntimeError(f"Portacode bootstrap steps failed: {summary}{history_snippet}")
910
947
  raise RuntimeError("Portacode bootstrap steps failed.")
911
948
  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
949
+ public_key = key_step.get("public_key") if key_step else default_public_key
913
950
  if not public_key:
914
951
  raise RuntimeError("Portacode connect did not return a public key.")
915
952
  return public_key, results
@@ -1053,16 +1090,19 @@ class CreateProxmoxContainerHandler(SyncHandler):
1053
1090
  return "create_proxmox_container"
1054
1091
 
1055
1092
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1093
+ logger.info("create_proxmox_container command received")
1056
1094
  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
1095
+ device_id = message.get("device_id")
1096
+ device_public_key = (message.get("device_public_key") or "").strip()
1097
+ device_private_key = (message.get("device_private_key") or "").strip()
1098
+ has_device_keypair = bool(device_public_key and device_private_key)
1064
1099
  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)
1100
+ bootstrap_steps = _build_bootstrap_steps(
1101
+ bootstrap_user,
1102
+ bootstrap_password,
1103
+ bootstrap_ssh_key,
1104
+ include_portacode_connect=not has_device_keypair,
1105
+ )
1066
1106
  total_steps = 3 + len(bootstrap_steps) + 2
1067
1107
  current_step_index = 1
1068
1108
 
@@ -1084,7 +1124,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1084
1124
  message=start_message,
1085
1125
  phase="lifecycle",
1086
1126
  request_id=request_id,
1087
- client_sessions=client_sessions,
1088
1127
  )
1089
1128
  try:
1090
1129
  result = action()
@@ -1099,7 +1138,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1099
1138
  message=f"{step_label} failed: {exc}",
1100
1139
  phase="lifecycle",
1101
1140
  request_id=request_id,
1102
- client_sessions=client_sessions,
1103
1141
  details={"error": str(exc)},
1104
1142
  )
1105
1143
  raise
@@ -1113,7 +1151,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1113
1151
  message=success_message,
1114
1152
  phase="lifecycle",
1115
1153
  request_id=request_id,
1116
- client_sessions=client_sessions,
1117
1154
  )
1118
1155
  current_step_index += 1
1119
1156
  return result
@@ -1143,7 +1180,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
1143
1180
  payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
1144
1181
  payload["memory"] = int(payload["ram_mib"])
1145
1182
  payload["node"] = node
1146
- logger.info(
1183
+ logger.debug(
1147
1184
  "Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
1148
1185
  node,
1149
1186
  payload["template"],
@@ -1156,12 +1193,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
1156
1193
  payload["created_at"] = datetime.utcnow().isoformat() + "Z"
1157
1194
  payload["status"] = "creating"
1158
1195
  _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
1196
  return proxmox, node, vmid, payload
1166
1197
 
1167
1198
  proxmox, node, vmid, payload = _run_lifecycle_step(
@@ -1220,16 +1251,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
1220
1251
  message=message_text,
1221
1252
  phase="bootstrap",
1222
1253
  request_id=request_id,
1223
- client_sessions=client_sessions,
1224
1254
  details=details or None,
1225
1255
  )
1226
1256
 
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
1257
  public_key, steps = _bootstrap_portacode(
1234
1258
  vmid,
1235
1259
  payload["username"],
@@ -1239,10 +1263,103 @@ class CreateProxmoxContainerHandler(SyncHandler):
1239
1263
  progress_callback=_bootstrap_progress_callback,
1240
1264
  start_index=current_step_index,
1241
1265
  total_steps=total_steps,
1266
+ default_public_key=device_public_key if has_device_keypair else None,
1242
1267
  )
1243
1268
  current_step_index += len(bootstrap_steps)
1244
1269
 
1245
- response = {
1270
+ service_installed = False
1271
+ if has_device_keypair:
1272
+ logger.info(
1273
+ "deploying dashboard-provided Portacode keypair (device_id=%s) into container %s",
1274
+ device_id,
1275
+ vmid,
1276
+ )
1277
+ _deploy_device_keypair(
1278
+ vmid,
1279
+ payload["username"],
1280
+ device_private_key,
1281
+ device_public_key,
1282
+ )
1283
+ service_installed = True
1284
+ service_start_index = current_step_index
1285
+
1286
+ auth_step_name = "setup_device_authentication"
1287
+ auth_label = "Setting up device authentication"
1288
+ _emit_progress_event(
1289
+ self,
1290
+ step_index=service_start_index,
1291
+ total_steps=total_steps,
1292
+ step_name=auth_step_name,
1293
+ step_label=auth_label,
1294
+ status="in_progress",
1295
+ message="Notifying the server of the new device…",
1296
+ phase="service",
1297
+ request_id=request_id,
1298
+ )
1299
+ _emit_progress_event(
1300
+ self,
1301
+ step_index=service_start_index,
1302
+ total_steps=total_steps,
1303
+ step_name=auth_step_name,
1304
+ step_label=auth_label,
1305
+ status="completed",
1306
+ message="Authentication metadata recorded.",
1307
+ phase="service",
1308
+ request_id=request_id,
1309
+ )
1310
+
1311
+ install_step = service_start_index + 1
1312
+ install_label = "Launching Portacode service"
1313
+ _emit_progress_event(
1314
+ self,
1315
+ step_index=install_step,
1316
+ total_steps=total_steps,
1317
+ step_name="launch_portacode_service",
1318
+ step_label=install_label,
1319
+ status="in_progress",
1320
+ message="Running sudo portacode service install…",
1321
+ phase="service",
1322
+ request_id=request_id,
1323
+ )
1324
+
1325
+ cmd = f"su - {payload['username']} -c 'sudo -S portacode service install'"
1326
+ res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
1327
+
1328
+ if res["returncode"] != 0:
1329
+ _emit_progress_event(
1330
+ self,
1331
+ step_index=install_step,
1332
+ total_steps=total_steps,
1333
+ step_name="launch_portacode_service",
1334
+ step_label=install_label,
1335
+ status="failed",
1336
+ message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
1337
+ phase="service",
1338
+ request_id=request_id,
1339
+ details={
1340
+ "stderr": res.get("stderr"),
1341
+ "stdout": res.get("stdout"),
1342
+ },
1343
+ )
1344
+ raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
1345
+
1346
+ _emit_progress_event(
1347
+ self,
1348
+ step_index=install_step,
1349
+ total_steps=total_steps,
1350
+ step_name="launch_portacode_service",
1351
+ step_label=install_label,
1352
+ status="completed",
1353
+ message="Portacode service install finished.",
1354
+ phase="service",
1355
+ request_id=request_id,
1356
+ )
1357
+
1358
+ logger.info("create_proxmox_container: portacode service install completed inside ct %s", vmid)
1359
+
1360
+ current_step_index += 2
1361
+
1362
+ return {
1246
1363
  "event": "proxmox_container_created",
1247
1364
  "success": True,
1248
1365
  "message": f"Container {vmid} is ready and Portacode key captured.",
@@ -1258,11 +1375,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
1258
1375
  "cpus": payload["cpus"],
1259
1376
  },
1260
1377
  "setup_steps": steps,
1378
+ "device_id": device_id,
1379
+ "service_installed": service_installed,
1261
1380
  }
1262
- if client_sessions:
1263
- response["client_sessions"] = client_sessions
1264
- response["_reply_to_source_session"] = True
1265
- return response
1266
1381
 
1267
1382
 
1268
1383
  class StartPortacodeServiceHandler(SyncHandler):
@@ -1290,15 +1405,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1290
1405
  start_index = int(message.get("step_index", 1))
1291
1406
  total_steps = int(message.get("total_steps", start_index + 2))
1292
1407
  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
1408
 
1303
1409
  auth_step_name = "setup_device_authentication"
1304
1410
  auth_label = "Setting up device authentication"
@@ -1312,7 +1418,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1312
1418
  message="Notifying the server of the new device…",
1313
1419
  phase="service",
1314
1420
  request_id=request_id,
1315
- client_sessions=client_sessions,
1316
1421
  )
1317
1422
  _emit_progress_event(
1318
1423
  self,
@@ -1324,7 +1429,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1324
1429
  message="Authentication metadata recorded.",
1325
1430
  phase="service",
1326
1431
  request_id=request_id,
1327
- client_sessions=client_sessions,
1328
1432
  )
1329
1433
 
1330
1434
  install_step = start_index + 1
@@ -1339,7 +1443,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1339
1443
  message="Running sudo portacode service install…",
1340
1444
  phase="service",
1341
1445
  request_id=request_id,
1342
- client_sessions=client_sessions,
1343
1446
  )
1344
1447
 
1345
1448
  cmd = f"su - {user} -c 'sudo -S portacode service install'"
@@ -1356,7 +1459,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1356
1459
  message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
1357
1460
  phase="service",
1358
1461
  request_id=request_id,
1359
- client_sessions=client_sessions,
1360
1462
  details={
1361
1463
  "stderr": res.get("stderr"),
1362
1464
  "stdout": res.get("stdout"),
@@ -1364,12 +1466,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1364
1466
  )
1365
1467
  raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
1366
1468
 
1367
- logger.info(
1368
- "portacode service install command completed vmid=%s returncode=%s",
1369
- vmid,
1370
- res["returncode"],
1371
- )
1372
-
1373
1469
  _emit_progress_event(
1374
1470
  self,
1375
1471
  step_index=install_step,
@@ -1380,19 +1476,14 @@ class StartPortacodeServiceHandler(SyncHandler):
1380
1476
  message="Portacode service install finished.",
1381
1477
  phase="service",
1382
1478
  request_id=request_id,
1383
- client_sessions=client_sessions,
1384
1479
  )
1385
1480
 
1386
- response = {
1481
+ return {
1387
1482
  "event": "proxmox_service_started",
1388
1483
  "success": True,
1389
1484
  "message": "Portacode service install completed",
1390
1485
  "ctid": str(vmid),
1391
1486
  }
1392
- if client_sessions:
1393
- response["client_sessions"] = client_sessions
1394
- response["_reply_to_source_session"] = True
1395
- return response
1396
1487
 
1397
1488
 
1398
1489
  class StartProxmoxContainerHandler(SyncHandler):
@@ -1408,20 +1499,12 @@ class StartProxmoxContainerHandler(SyncHandler):
1408
1499
  proxmox = _connect_proxmox(config)
1409
1500
  node = _get_node_from_config(config)
1410
1501
  _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
1502
 
1420
1503
  status, elapsed = _start_container(proxmox, node, vmid)
1421
1504
  _update_container_record(vmid, {"status": "running"})
1422
1505
 
1423
1506
  infra = get_infra_snapshot()
1424
- response = {
1507
+ return {
1425
1508
  "event": "proxmox_container_action",
1426
1509
  "action": "start",
1427
1510
  "success": True,
@@ -1431,10 +1514,6 @@ class StartProxmoxContainerHandler(SyncHandler):
1431
1514
  "status": status.get("status"),
1432
1515
  "infra": infra,
1433
1516
  }
1434
- if client_sessions:
1435
- response["client_sessions"] = client_sessions
1436
- response["_reply_to_source_session"] = True
1437
- return response
1438
1517
 
1439
1518
 
1440
1519
  class StopProxmoxContainerHandler(SyncHandler):
@@ -1450,14 +1529,6 @@ class StopProxmoxContainerHandler(SyncHandler):
1450
1529
  proxmox = _connect_proxmox(config)
1451
1530
  node = _get_node_from_config(config)
1452
1531
  _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
1532
 
1462
1533
  status, elapsed = _stop_container(proxmox, node, vmid)
1463
1534
  final_status = status.get("status") or "stopped"
@@ -1469,7 +1540,7 @@ class StopProxmoxContainerHandler(SyncHandler):
1469
1540
  if final_status != "running" and elapsed == 0.0
1470
1541
  else f"Stopped container {vmid} in {elapsed:.1f}s."
1471
1542
  )
1472
- response = {
1543
+ return {
1473
1544
  "event": "proxmox_container_action",
1474
1545
  "action": "stop",
1475
1546
  "success": True,
@@ -1479,10 +1550,6 @@ class StopProxmoxContainerHandler(SyncHandler):
1479
1550
  "status": final_status,
1480
1551
  "infra": infra,
1481
1552
  }
1482
- if client_sessions:
1483
- response["client_sessions"] = client_sessions
1484
- response["_reply_to_source_session"] = True
1485
- return response
1486
1553
 
1487
1554
 
1488
1555
  class RemoveProxmoxContainerHandler(SyncHandler):
@@ -1498,21 +1565,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1498
1565
  proxmox = _connect_proxmox(config)
1499
1566
  node = _get_node_from_config(config)
1500
1567
  _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
1568
 
1510
1569
  stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
1511
1570
  delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
1512
1571
  _remove_container_record(vmid)
1513
1572
 
1514
1573
  infra = get_infra_snapshot()
1515
- response = {
1574
+ return {
1516
1575
  "event": "proxmox_container_action",
1517
1576
  "action": "remove",
1518
1577
  "success": True,
@@ -1525,10 +1584,6 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1525
1584
  "status": "deleted",
1526
1585
  "infra": infra,
1527
1586
  }
1528
- if client_sessions:
1529
- response["client_sessions"] = client_sessions
1530
- response["_reply_to_source_session"] = True
1531
- return response
1532
1587
 
1533
1588
 
1534
1589
  class ConfigureProxmoxInfraHandler(SyncHandler):
@@ -1542,19 +1597,13 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
1542
1597
  verify_ssl = bool(message.get("verify_ssl"))
1543
1598
  if not token_identifier or not token_value:
1544
1599
  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
1600
  snapshot = configure_infrastructure(token_identifier, token_value, verify_ssl=verify_ssl)
1548
- response = {
1601
+ return {
1549
1602
  "event": "proxmox_infra_configured",
1550
1603
  "success": True,
1551
1604
  "message": "Proxmox infrastructure configured",
1552
1605
  "infra": snapshot,
1553
1606
  }
1554
- if client_sessions:
1555
- response["client_sessions"] = client_sessions
1556
- response["_reply_to_source_session"] = True
1557
- return response
1558
1607
 
1559
1608
 
1560
1609
  class RevertProxmoxInfraHandler(SyncHandler):
@@ -1563,16 +1612,10 @@ class RevertProxmoxInfraHandler(SyncHandler):
1563
1612
  return "revert_proxmox_infra"
1564
1613
 
1565
1614
  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
1615
  snapshot = revert_infrastructure()
1569
- response = {
1616
+ return {
1570
1617
  "event": "proxmox_infra_reverted",
1571
1618
  "success": True,
1572
1619
  "message": "Proxmox infrastructure configuration reverted",
1573
1620
  "infra": snapshot,
1574
1621
  }
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.dev3
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=e6xwmilPep3lYl64LRXaySWQ_Gewk6WD90oO1hH3ckU,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=I9DzvhLDquUHGt_5e_AmmWSsIWka-O7kdWjMhonJ2b4,58984
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.dev3.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.dev3.dist-info/METADATA,sha256=vNRk53o0J8gYkSuPqsZO-3Aqhz86AAQwdRNEAf1s26U,13051
95
+ portacode-1.4.13.dev3.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
96
+ portacode-1.4.13.dev3.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
97
+ portacode-1.4.13.dev3.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
98
+ portacode-1.4.13.dev3.dist-info/RECORD,,