portacode 2.0.0.dev0__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. {portacode-2.0.0.dev0 → portacode-2.0.2}/PKG-INFO +1 -1
  2. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/_version.py +2 -2
  3. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/guest_host_protocol.py +13 -3
  4. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +20 -0
  5. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/__init__.py +2 -0
  6. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/proxmox_infra.py +107 -37
  7. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/registry.py +31 -0
  8. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/system_handlers.py +1 -1
  9. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_proxmox_infra.py +71 -0
  10. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/terminal.py +11 -6
  11. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/test_guest_host_terminal.py +43 -3
  12. portacode-2.0.2/portacode/tests/test_terminal_periodic_system_info.py +59 -0
  13. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/PKG-INFO +1 -1
  14. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/SOURCES.txt +1 -0
  15. {portacode-2.0.0.dev0 → portacode-2.0.2}/.gitignore +0 -0
  16. {portacode-2.0.0.dev0 → portacode-2.0.2}/AGENTS.md +0 -0
  17. {portacode-2.0.0.dev0 → portacode-2.0.2}/LICENSE +0 -0
  18. {portacode-2.0.0.dev0 → portacode-2.0.2}/MANIFEST.in +0 -0
  19. {portacode-2.0.0.dev0 → portacode-2.0.2}/Makefile +0 -0
  20. {portacode-2.0.0.dev0 → portacode-2.0.2}/README.md +0 -0
  21. {portacode-2.0.0.dev0 → portacode-2.0.2}/build_android.sh +0 -0
  22. {portacode-2.0.0.dev0 → portacode-2.0.2}/connect.py +0 -0
  23. {portacode-2.0.0.dev0 → portacode-2.0.2}/connect.sh +0 -0
  24. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
  25. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/creative-team-brief-portacode.md +0 -0
  26. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/device-health-alerting-plan.md +0 -0
  27. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/devops-messaging-ab-tests.md +0 -0
  28. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/guest-host-requests-plan.md +0 -0
  29. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/homepage-dashboard-positioning-fixes.md +0 -0
  30. {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/template-guide.html +0 -0
  31. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/README.md +0 -0
  32. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/Dockerfile +0 -0
  33. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/README.md +0 -0
  34. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
  35. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  36. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/docker-compose.yaml +0 -0
  37. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/Dockerfile +0 -0
  38. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/README.md +0 -0
  39. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
  40. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  41. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
  42. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
  43. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
  44. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
  45. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
  46. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
  47. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
  48. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
  49. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
  50. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
  51. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
  52. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
  53. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
  54. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
  55. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
  56. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
  57. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
  58. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
  59. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
  60. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
  61. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
  62. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
  63. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
  64. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
  65. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
  66. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
  67. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
  68. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
  69. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  70. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
  71. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
  72. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
  73. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
  74. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
  75. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
  76. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
  77. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
  78. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
  79. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
  80. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
  81. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
  82. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
  83. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
  84. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
  85. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
  86. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
  87. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
  88. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
  89. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
  90. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
  91. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
  92. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
  93. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
  94. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
  95. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
  96. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
  97. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
  98. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
  99. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
  100. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
  101. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
  102. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
  103. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
  104. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
  105. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
  106. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
  107. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
  108. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
  109. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
  110. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
  111. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
  112. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
  113. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
  114. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
  115. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
  116. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
  117. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
  118. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
  119. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
  120. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
  121. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
  122. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
  123. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
  124. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
  125. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
  126. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
  127. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
  128. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
  129. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
  130. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
  131. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
  132. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
  133. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
  134. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
  135. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
  136. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
  137. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
  138. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
  139. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
  140. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
  141. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
  142. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
  143. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
  144. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
  145. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
  146. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
  147. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
  148. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
  149. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
  150. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
  151. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
  152. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
  153. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
  154. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
  155. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
  156. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
  157. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
  158. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
  159. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
  160. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
  161. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
  162. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
  163. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
  164. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
  165. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
  166. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
  167. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
  168. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
  169. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
  170. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
  171. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
  172. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
  173. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
  174. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
  175. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
  176. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
  177. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
  178. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
  179. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
  180. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
  181. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
  182. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
  183. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
  184. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
  185. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
  186. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
  187. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
  188. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
  189. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
  190. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
  191. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
  192. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
  193. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
  194. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
  195. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
  196. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
  197. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
  198. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
  199. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
  200. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
  201. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
  202. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
  203. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
  204. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
  205. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
  206. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
  207. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
  208. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
  209. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
  210. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
  211. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
  212. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
  213. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
  214. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
  215. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
  216. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
  217. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
  218. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
  219. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
  220. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
  221. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
  222. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
  223. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
  224. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
  225. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
  226. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
  227. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
  228. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
  229. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
  230. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
  231. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
  232. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
  233. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
  234. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
  235. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
  236. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
  237. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
  238. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
  239. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
  240. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
  241. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
  242. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
  243. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
  244. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
  245. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
  246. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
  247. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
  248. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
  249. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
  250. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
  251. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
  252. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
  253. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
  254. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/docker-compose.yaml +0 -0
  255. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/README.md +0 -0
  256. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
  257. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
  258. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
  259. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
  260. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
  261. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/manage.py +0 -0
  262. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
  263. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
  264. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
  265. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
  266. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
  267. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
  268. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
  269. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
  270. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
  271. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
  272. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
  273. {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
  274. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/README.md +0 -0
  275. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/__init__.py +0 -0
  276. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/__main__.py +0 -0
  277. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/cli.py +0 -0
  278. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/codex_loopback_proxy.py +0 -0
  279. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/codex_prepare.py +0 -0
  280. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/codex_usage_limit.py +0 -0
  281. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/README.md +0 -0
  282. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/__init__.py +0 -0
  283. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/client.py +0 -0
  284. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/codex_app_server.py +0 -0
  285. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/README.md +0 -0
  286. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
  287. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/base.py +0 -0
  288. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/browser_handlers.py +0 -0
  289. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/chunked_content.py +0 -0
  290. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
  291. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
  292. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/codex_handlers.py +0 -0
  293. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/diff_handlers.py +0 -0
  294. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/file_handlers.py +0 -0
  295. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
  296. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  297. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/README.md +0 -0
  298. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/__init__.py +0 -0
  299. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  300. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/git_manager.py +0 -0
  301. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/handlers.py +0 -0
  302. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/manager.py +0 -0
  303. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/models.py +0 -0
  304. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
  305. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/utils.py +0 -0
  306. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state_handlers.py +0 -0
  307. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/resumable_transfer_handlers.py +0 -0
  308. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/runtime_user.py +0 -0
  309. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/session.py +0 -0
  310. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/tab_factory.py +0 -0
  311. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/terminal_handlers.py +0 -0
  312. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_browser_handlers.py +0 -0
  313. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
  314. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_file_move_copy.py +0 -0
  315. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_runtime_user.py +0 -0
  316. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/update_handler.py +0 -0
  317. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/multiplex.py +0 -0
  318. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/test_guest_host_protocol.py +0 -0
  319. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/test_webmin_proxy_config.py +0 -0
  320. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/webmin_proxy_config.py +0 -0
  321. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/data.py +0 -0
  322. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/exit_codes.py +0 -0
  323. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/github_credential.py +0 -0
  324. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/keypair.py +0 -0
  325. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/__init__.py +0 -0
  326. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  327. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
  328. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  329. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
  330. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/elinks +0 -0
  331. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/gio-open +0 -0
  332. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/gnome-open +0 -0
  333. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/gvfs-open +0 -0
  334. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/kde-open +0 -0
  335. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/kfmclient +0 -0
  336. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
  337. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
  338. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/links +0 -0
  339. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/links2 +0 -0
  340. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/lynx +0 -0
  341. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/mate-open +0 -0
  342. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/netsurf +0 -0
  343. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/sensible-browser +0 -0
  344. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/w3m +0 -0
  345. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/x-www-browser +0 -0
  346. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/xdg-open +0 -0
  347. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/logging_categories.py +0 -0
  348. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/pairing.py +0 -0
  349. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/provisioning_cache.py +0 -0
  350. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/restart.py +0 -0
  351. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/service.py +0 -0
  352. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/singleton.py +0 -0
  353. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/static/js/test-ntp-clock.html +0 -0
  354. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/static/js/utils/ntp-clock.js +0 -0
  355. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_runtime_paths.py +0 -0
  356. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_service.py +0 -0
  357. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_singleton.py +0 -0
  358. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_terminal_exposed_services.py +0 -0
  359. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_app_server.py +0 -0
  360. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_handlers.py +0 -0
  361. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_loopback_proxy.py +0 -0
  362. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_prepare.py +0 -0
  363. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_github_credential.py +0 -0
  364. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_image_read_handler.py +0 -0
  365. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_resumable_transfer_handlers.py +0 -0
  366. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_system_handlers.py +0 -0
  367. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/__init__.py +0 -0
  368. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/cloudflared_login.py +0 -0
  369. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/ensure_cloudflared.py +0 -0
  370. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/ensure_pyyaml.py +0 -0
  371. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/forwarding_state.py +0 -0
  372. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/get_domain.py +0 -0
  373. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/privileged.py +0 -0
  374. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/service_install.py +0 -0
  375. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/state.py +0 -0
  376. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/updater.py +0 -0
  377. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  378. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/__init__.py +0 -0
  379. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/diff_apply.py +0 -0
  380. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/diff_renderer.py +0 -0
  381. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/ntp_clock.py +0 -0
  382. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/runtime_paths.py +0 -0
  383. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/dependency_links.txt +0 -0
  384. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/entry_points.txt +0 -0
  385. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/requires.txt +0 -0
  386. {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/top_level.txt +0 -0
  387. {portacode-2.0.0.dev0 → portacode-2.0.2}/pyproject.toml +0 -0
  388. {portacode-2.0.0.dev0 → portacode-2.0.2}/run_tests.py +0 -0
  389. {portacode-2.0.0.dev0 → portacode-2.0.2}/scripts/diagnose_codex_gateway.py +0 -0
  390. {portacode-2.0.0.dev0 → portacode-2.0.2}/setup.cfg +0 -0
  391. {portacode-2.0.0.dev0 → portacode-2.0.2}/setup.py +0 -0
  392. {portacode-2.0.0.dev0 → portacode-2.0.2}/test.sh +0 -0
  393. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/README.md +0 -0
  394. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/__init__.py +0 -0
  395. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_device_online.py +0 -0
  396. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_file_operations.py +0 -0
  397. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_git_status_ui.py +0 -0
  398. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_login_flow.py +0 -0
  399. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_navigate_testing_folder.py +0 -0
  400. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_play_store_screenshots.py +0 -0
  401. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_buffer_performance.py +0 -0
  402. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_interaction.py +0 -0
  403. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_loading_race_condition.py +0 -0
  404. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_start.py +0 -0
  405. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_request_id.py +0 -0
  406. {portacode-2.0.0.dev0 → portacode-2.0.2}/test_session_terminal_shutdown.py +0 -0
  407. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/.env.example +0 -0
  408. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/README.md +0 -0
  409. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/__init__.py +0 -0
  410. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/cli.py +0 -0
  411. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/__init__.py +0 -0
  412. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/base_test.py +0 -0
  413. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/cli_manager.py +0 -0
  414. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/hierarchical_runner.py +0 -0
  415. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/playwright_manager.py +0 -0
  416. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/runner.py +0 -0
  417. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/shared_cli_manager.py +0 -0
  418. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/test_discovery.py +0 -0
  419. {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/requirements.txt +0 -0
  420. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
  421. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
  422. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/agent_context_management.md +0 -0
  423. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/django_server_time_sync.md +0 -0
  424. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/device_performance_degradation.md +0 -0
  425. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
  426. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/indefinite_resource_loading.md +0 -0
  427. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/portacode_service_silently_down.md +0 -0
  428. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/premature_terminal_exit.md +0 -0
  429. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/project_cpu_hotspots.md +0 -0
  430. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/terminals_exit_upon_starting.md +0 -0
  431. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/websocket_client_silently_dead.md +0 -0
  432. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
  433. {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/smartphone_terminal_input_frustrations.md +0 -0
  434. {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/generate_play_store_assets.py +0 -0
  435. {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/pairing_tester.py +0 -0
  436. {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/run_screenshot_suite.sh +0 -0
  437. {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/test_python_ntp_clock.py +0 -0
  438. {portacode-2.0.0.dev0 → portacode-2.0.2}/validate.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 2.0.0.dev0
3
+ Version: 2.0.2
4
4
  Summary: Device agent for the AI platform that builds, tests, deploys, and operates real software
5
5
  Home-page: https://portacode.com/
6
6
  Author: Meena Erian
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.0.0.dev0'
22
- __version_tuple__ = version_tuple = (2, 0, 0, 'dev0')
21
+ __version__ = version = '2.0.2'
22
+ __version_tuple__ = version_tuple = (2, 0, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -24,6 +24,7 @@ GUEST_HOST_REQUEST_COMMANDS = frozenset(
24
24
  "configure_proxmox_container_expose_ports",
25
25
  "start_proxmox_container",
26
26
  "stop_proxmox_container",
27
+ "restart_proxmox_container",
27
28
  "remove_proxmox_container",
28
29
  "create_proxmox_container",
29
30
  }
@@ -32,14 +33,23 @@ GUEST_HOST_COMMAND_OPERATIONS = {
32
33
  "configure_proxmox_container_expose_ports": "expose_ports",
33
34
  "start_proxmox_container": "power",
34
35
  "stop_proxmox_container": "power",
36
+ "restart_proxmox_container": "power",
35
37
  "remove_proxmox_container": "delete",
36
38
  "create_proxmox_container": "provision",
37
39
  }
38
40
 
39
- # The protocol reserves additional commands for staged rollout, but this CLI
40
- # release advertises and executes only the operation validated end-to-end.
41
+ # Every advertised operation is mapped to its existing, independently
42
+ # registered legacy handler. Server-side capability and runtime gates still
43
+ # control which operations are actually sent through this protocol.
41
44
  GUEST_HOST_EXECUTABLE_COMMANDS = frozenset(
42
- {"configure_proxmox_container_expose_ports"}
45
+ {
46
+ "configure_proxmox_container_expose_ports",
47
+ "start_proxmox_container",
48
+ "stop_proxmox_container",
49
+ "restart_proxmox_container",
50
+ "remove_proxmox_container",
51
+ "create_proxmox_container",
52
+ }
43
53
  )
44
54
 
45
55
 
@@ -88,6 +88,18 @@ versions continue using existing commands while flags are disabled. Unknown
88
88
  message types or commands must fail closed; they must never fall through to an
89
89
  ordinary client-session handler.
90
90
 
91
+ Protocol version 1 defines these request types and command mappings:
92
+
93
+ - `expose_ports` → `configure_proxmox_container_expose_ports`;
94
+ - `power` → `start_proxmox_container`, `stop_proxmox_container`, or `restart_proxmox_container`;
95
+ - `delete` → `remove_proxmox_container`;
96
+ - `provision` → `create_proxmox_container`.
97
+
98
+ Supporting CLI versions advertise the enabled set in
99
+ `system_info.protocol_capabilities.guest_host_requests.request_types`. The
100
+ server must check that per-host list as well as its per-operation runtime flag
101
+ before selecting this transport. Otherwise it uses the unchanged legacy path.
102
+
91
103
  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.
92
104
 
93
105
  ## Table of Contents
@@ -534,6 +546,14 @@ Stops a running Portacode-managed container. Handled by [`StopProxmoxContainerHa
534
546
  * Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="stop"` and the refreshed infra snapshot.
535
547
  * Emits an [`error`](#error) event on failure.
536
548
 
549
+ ### `restart_proxmox_container`
550
+
551
+ Stops and starts a Portacode-managed container as one validated host-side
552
+ operation. It accepts the same required `child_device_id` and optional `ctid`
553
+ fields as the start/stop actions. The handler verifies the child-to-container
554
+ mapping before changing state, then emits `proxmox_container_action` with
555
+ `action="restart"`.
556
+
537
557
  ### `remove_proxmox_container`
538
558
 
539
559
  Deletes a managed container from Proxmox (stopping it first if necessary) and removes the stored metadata file. Handled by [`RemoveProxmoxContainerHandler`](./proxmox_infra.py).
@@ -60,6 +60,7 @@ from .proxmox_infra import (
60
60
  StartPortacodeServiceHandler,
61
61
  StartProxmoxContainerHandler,
62
62
  StopProxmoxContainerHandler,
63
+ RestartProxmoxContainerHandler,
63
64
  RemoveProxmoxContainerHandler,
64
65
  )
65
66
  from .cloudflare_tunnel import CloudflareTunnelSetupHandler
@@ -132,6 +133,7 @@ __all__ = [
132
133
  "StartPortacodeServiceHandler",
133
134
  "StartProxmoxContainerHandler",
134
135
  "StopProxmoxContainerHandler",
136
+ "RestartProxmoxContainerHandler",
135
137
  "RemoveProxmoxContainerHandler",
136
138
  "UpdatePortacodeHandler",
137
139
  "RevertProxmoxInfraHandler",
@@ -1327,7 +1327,7 @@ def _refresh_container_statuses(records: List[Dict[str, Any]], config: Optional[
1327
1327
 
1328
1328
 
1329
1329
  def _initialize_managed_containers_state(force: bool = False) -> Dict[str, Any]:
1330
- with _MANAGED_CONTAINERS_STATE_LOCK:
1330
+ with _acquire_state_lock("managed state initialization check"):
1331
1331
  if _MANAGED_CONTAINERS_STATE["initialized"] and not force:
1332
1332
  base = _MANAGED_CONTAINERS_STATE["base_summary"]
1333
1333
  return _copy_summary(base) if base else {}
@@ -1352,7 +1352,7 @@ def _initialize_managed_containers_state(force: bool = False) -> Dict[str, Any]:
1352
1352
  "cpu_share": float(base_summary.get("total_cpu_share") or 0.0),
1353
1353
  }
1354
1354
 
1355
- with _MANAGED_CONTAINERS_STATE_LOCK:
1355
+ with _acquire_state_lock("managed state initialization commit"):
1356
1356
  _MANAGED_CONTAINERS_STATE["initialized"] = True
1357
1357
  _MANAGED_CONTAINERS_STATE["base_summary"] = base_summary
1358
1358
  _MANAGED_CONTAINERS_STATE["initial_totals"] = initial_totals
@@ -1374,7 +1374,7 @@ def reconcile_managed_containers_inventory() -> Dict[str, Any]:
1374
1374
  never replaced by reconciliation.
1375
1375
  """
1376
1376
  _initialize_managed_containers_state()
1377
- with _MANAGED_CONTAINERS_STATE_LOCK:
1377
+ with _acquire_state_lock("inventory reconciliation snapshot"):
1378
1378
  revision = int(_MANAGED_CONTAINERS_STATE.get("revision") or 0)
1379
1379
  records = [record.copy() for record in _MANAGED_CONTAINERS_STATE.get("records", {}).values()]
1380
1380
 
@@ -1401,38 +1401,46 @@ def reconcile_managed_containers_inventory() -> Dict[str, Any]:
1401
1401
 
1402
1402
  cleaned_vmids: List[int] = []
1403
1403
  cleanup_errors: List[Dict[str, str]] = []
1404
- with _MANAGED_CONTAINERS_STATE_LOCK:
1404
+ with _acquire_state_lock("inventory reconciliation pre-cleanup check"):
1405
1405
  if int(_MANAGED_CONTAINERS_STATE.get("revision") or 0) != revision:
1406
1406
  return {"refreshed": False, "reason": "state_changed"}
1407
- # Serialize cleanup with create/delete state mutations. This prevents a
1408
- # stale inventory scan from clearing metadata after a CTID is reused.
1409
- for record in missing_records:
1410
- try:
1411
- vmid = int(record["vmid"])
1412
- except (KeyError, TypeError, ValueError):
1413
- continue
1414
- device_id = str(record.get("device_id") or "").strip()
1415
- try:
1416
- if device_id:
1417
- from .cloudflare_forwarding import set_container_forwarding_rules
1418
1407
 
1419
- try:
1420
- set_container_forwarding_rules(device_id, [])
1421
- except RuntimeError as exc:
1422
- message = str(exc).lower()
1423
- if (
1424
- "cloudflare tunnel is not configured yet" not in message
1425
- and "domain or tunnel name missing" not in message
1426
- ):
1427
- raise
1428
- cleaned_vmids.append(vmid)
1429
- except Exception as exc: # retry on the next reconciliation pass
1430
- logger.warning(
1431
- "Unable to clean Proxmox-side metadata for missing container %s: %s",
1432
- vmid,
1433
- exc,
1434
- )
1435
- cleanup_errors.append({"vmid": str(vmid), "error": str(exc)})
1408
+ # Forwarding cleanup may call cloudflared and touch its configuration. It
1409
+ # must never run while holding the managed-state lock: reservations,
1410
+ # deletion, and system snapshots all need that lock to remain responsive.
1411
+ for record in missing_records:
1412
+ try:
1413
+ vmid = int(record["vmid"])
1414
+ except (KeyError, TypeError, ValueError):
1415
+ continue
1416
+ device_id = str(record.get("device_id") or "").strip()
1417
+ try:
1418
+ if device_id:
1419
+ from .cloudflare_forwarding import set_container_forwarding_rules
1420
+
1421
+ try:
1422
+ set_container_forwarding_rules(device_id, [])
1423
+ except RuntimeError as exc:
1424
+ message = str(exc).lower()
1425
+ if (
1426
+ "cloudflare tunnel is not configured yet" not in message
1427
+ and "domain or tunnel name missing" not in message
1428
+ ):
1429
+ raise
1430
+ cleaned_vmids.append(vmid)
1431
+ except Exception as exc: # retry on the next reconciliation pass
1432
+ logger.warning(
1433
+ "Unable to clean Proxmox-side metadata for missing container %s: %s",
1434
+ vmid,
1435
+ exc,
1436
+ )
1437
+ cleanup_errors.append({"vmid": str(vmid), "error": str(exc)})
1438
+
1439
+ with _acquire_state_lock("inventory reconciliation commit"):
1440
+ if int(_MANAGED_CONTAINERS_STATE.get("revision") or 0) != revision:
1441
+ return {"refreshed": False, "reason": "state_changed"}
1442
+ # The revision check prevents stale scan results from removing metadata
1443
+ # after provisioning or deletion changed the managed state.
1436
1444
  for vmid in cleaned_vmids:
1437
1445
  path = CONTAINERS_DIR / f"ct-{vmid}.json"
1438
1446
  path.unlink(missing_ok=True)
@@ -1856,7 +1864,7 @@ def _compose_managed_containers_summary(
1856
1864
 
1857
1865
  def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
1858
1866
  _initialize_managed_containers_state(force=force)
1859
- with _MANAGED_CONTAINERS_STATE_LOCK:
1867
+ with _acquire_state_lock("managed container summary snapshot"):
1860
1868
  base_summary = _MANAGED_CONTAINERS_STATE.get("base_summary") or {}
1861
1869
  records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
1862
1870
  pending = list(_MANAGED_CONTAINERS_STATE.get("pending", {}).values())
@@ -1872,7 +1880,7 @@ def _reserve_container_resources(payload: Dict[str, Any], *, device_id: str, req
1872
1880
  disk_gib = int(payload.get("disk_gib") or 0)
1873
1881
  cpu_share = float(payload.get("cpus") or 0.0)
1874
1882
  reservation_id = secrets.token_hex(8)
1875
- with _MANAGED_CONTAINERS_STATE_LOCK:
1883
+ with _acquire_state_lock("container resource reservation"):
1876
1884
  base_summary = _MANAGED_CONTAINERS_STATE.get("base_summary") or {}
1877
1885
  records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
1878
1886
  pending = list(_MANAGED_CONTAINERS_STATE.get("pending", {}).values())
@@ -1904,7 +1912,7 @@ def _reserve_container_resources(payload: Dict[str, Any], *, device_id: str, req
1904
1912
  def _release_container_reservation(reservation_id: Optional[str]) -> None:
1905
1913
  if not reservation_id:
1906
1914
  return
1907
- with _MANAGED_CONTAINERS_STATE_LOCK:
1915
+ with _acquire_state_lock("container reservation release"):
1908
1916
  _MANAGED_CONTAINERS_STATE.get("pending", {}).pop(reservation_id, None)
1909
1917
 
1910
1918
 
@@ -2990,7 +2998,7 @@ def _require_successful_proxmox_task(
2990
2998
 
2991
2999
  def _register_container_record(vmid: int, payload: Dict[str, Any], reservation_id: Optional[str] = None) -> None:
2992
3000
  _initialize_managed_containers_state()
2993
- with _MANAGED_CONTAINERS_STATE_LOCK:
3001
+ with _acquire_state_lock("container record update"):
2994
3002
  if reservation_id:
2995
3003
  _MANAGED_CONTAINERS_STATE["pending"].pop(reservation_id, None)
2996
3004
  _MANAGED_CONTAINERS_STATE["records"][str(vmid)] = payload.copy()
@@ -3001,7 +3009,7 @@ def _register_container_record(vmid: int, payload: Dict[str, Any], reservation_i
3001
3009
 
3002
3010
  def _unregister_container_record(vmid: int) -> None:
3003
3011
  _initialize_managed_containers_state()
3004
- with _MANAGED_CONTAINERS_STATE_LOCK:
3012
+ with _acquire_state_lock("container record removal"):
3005
3013
  _MANAGED_CONTAINERS_STATE["records"].pop(str(vmid), None)
3006
3014
  _MANAGED_CONTAINERS_STATE["revision"] = int(
3007
3015
  _MANAGED_CONTAINERS_STATE.get("revision") or 0
@@ -5165,6 +5173,68 @@ class StopProxmoxContainerHandler(SyncHandler):
5165
5173
  }
5166
5174
 
5167
5175
 
5176
+ class RestartProxmoxContainerHandler(SyncHandler):
5177
+ """Restart a managed container as one validated host-side operation."""
5178
+
5179
+ @property
5180
+ def command_name(self) -> str:
5181
+ return "restart_proxmox_container"
5182
+
5183
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
5184
+ child_device_id = (message.get("child_device_id") or "").strip()
5185
+ if not child_device_id:
5186
+ raise ValueError("child_device_id is required for restart_proxmox_container")
5187
+ config = _ensure_infra_configured()
5188
+ proxmox = _connect_proxmox(config)
5189
+ node = _get_node_from_config(config)
5190
+ try:
5191
+ vmid = _parse_ctid(message)
5192
+ except ValueError:
5193
+ try:
5194
+ vmid = _resolve_vmid_for_device(child_device_id)
5195
+ except _DeviceLookupError:
5196
+ vmid = _resolve_vmid_for_device_in_proxmox(proxmox, node, child_device_id)
5197
+ _ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
5198
+
5199
+ stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
5200
+ start_status, start_elapsed = _start_container(proxmox, node, vmid)
5201
+ _update_container_record(vmid, {"status": "running"})
5202
+
5203
+ forwarding_ip_updated = False
5204
+ try:
5205
+ from .cloudflare_forwarding import reconcile_container_forwarding_ip
5206
+
5207
+ forwarding_ip_updated = reconcile_container_forwarding_ip(
5208
+ child_device_id,
5209
+ proxmox=proxmox,
5210
+ node=node,
5211
+ lease_attempts=6,
5212
+ lease_retry_seconds=1.0,
5213
+ )
5214
+ except Exception:
5215
+ logger.warning(
5216
+ "Unable to reconcile exposed IP after restarting container device_id=%s vmid=%s",
5217
+ child_device_id,
5218
+ vmid,
5219
+ exc_info=True,
5220
+ )
5221
+
5222
+ return {
5223
+ "event": "proxmox_container_action",
5224
+ "action": "restart",
5225
+ "success": True,
5226
+ "ctid": str(vmid),
5227
+ "message": f"Restarted container {vmid} in {stop_elapsed + start_elapsed:.1f}s.",
5228
+ "details": {
5229
+ "stop_exitstatus": stop_status.get("exitstatus"),
5230
+ "start_exitstatus": start_status.get("exitstatus"),
5231
+ "forwarding_ip_updated": forwarding_ip_updated,
5232
+ },
5233
+ "status": start_status.get("status") or "running",
5234
+ "infra": get_infra_snapshot(),
5235
+ }
5236
+
5237
+
5168
5238
  class RemoveProxmoxContainerHandler(SyncHandler):
5169
5239
  """Delete a managed container via the Proxmox API."""
5170
5240
 
@@ -1,5 +1,6 @@
1
1
  """Command registry for managing handler dispatch."""
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  from typing import Dict, Type, Any, Optional, List, TYPE_CHECKING
5
6
  from portacode.utils.ntp_clock import ntp_clock
@@ -24,6 +25,7 @@ class CommandRegistry:
24
25
  self.control_channel = control_channel
25
26
  self.context = context
26
27
  self._handlers: Dict[str, "BaseHandler"] = {}
28
+ self._inflight_dispatches: Dict[str, asyncio.Task[bool]] = {}
27
29
 
28
30
  def register(self, handler_class: Type["BaseHandler"]) -> None:
29
31
  """Register a command handler.
@@ -99,6 +101,35 @@ class CommandRegistry:
99
101
  logger.warning("registry: No handler found for command: %s", command_name)
100
102
  return False
101
103
 
104
+ # System information is requested by periodic refreshes, dashboard
105
+ # joins, and explicit probes. All of those sources must share one
106
+ # collection; Proxmox discovery is too expensive to run concurrently.
107
+ if command_name == "system_info":
108
+ existing = self._inflight_dispatches.get(command_name)
109
+ if existing is not None and not existing.done():
110
+ logger.info("registry: Coalesced concurrent system_info request")
111
+ return True
112
+ task = asyncio.create_task(
113
+ self._dispatch_handler(command_name, handler, message, reply_channel)
114
+ )
115
+ self._inflight_dispatches[command_name] = task
116
+
117
+ def _clear_inflight(completed: asyncio.Task[bool]) -> None:
118
+ if self._inflight_dispatches.get(command_name) is completed:
119
+ self._inflight_dispatches.pop(command_name, None)
120
+
121
+ task.add_done_callback(_clear_inflight)
122
+ return True
123
+
124
+ return await self._dispatch_handler(command_name, handler, message, reply_channel)
125
+
126
+ async def _dispatch_handler(
127
+ self,
128
+ command_name: str,
129
+ handler: "BaseHandler",
130
+ message: Dict[str, Any],
131
+ reply_channel: Optional[str],
132
+ ) -> bool:
102
133
  try:
103
134
  await handler.handle(message, reply_channel)
104
135
  logger.info("registry: Successfully dispatched command '%s'", command_name)
@@ -632,7 +632,7 @@ class SystemInfoHandler(SyncHandler):
632
632
  info["protocol_capabilities"] = {
633
633
  "guest_host_requests": {
634
634
  "protocol_version": 1,
635
- "request_types": ["expose_ports"],
635
+ "request_types": ["expose_ports", "power", "delete", "provision"],
636
636
  "message_types": [
637
637
  "guest_host_request",
638
638
  "guest_host_ack",
@@ -1,10 +1,12 @@
1
1
  from unittest import TestCase
2
+ import threading
2
3
  from pathlib import Path
3
4
  from tempfile import TemporaryDirectory
4
5
  from unittest.mock import MagicMock, patch
5
6
 
6
7
  from portacode.connection.handlers.proxmox_infra import (
7
8
  RemoveProxmoxContainerHandler,
9
+ RestartProxmoxContainerHandler,
8
10
  _build_full_container_summary,
9
11
  _compose_managed_containers_summary,
10
12
  _build_bootstrap_steps,
@@ -36,6 +38,31 @@ from portacode.connection.handlers.proxmox_infra import (
36
38
 
37
39
 
38
40
  class ProxmoxInfraHandlerTests(TestCase):
41
+ @patch("portacode.connection.handlers.proxmox_infra.get_infra_snapshot", return_value={})
42
+ @patch("portacode.connection.handlers.proxmox_infra._update_container_record")
43
+ @patch("portacode.connection.handlers.proxmox_infra._start_container")
44
+ @patch("portacode.connection.handlers.proxmox_infra._stop_container")
45
+ @patch("portacode.connection.handlers.proxmox_infra._ensure_container_managed")
46
+ @patch("portacode.connection.handlers.proxmox_infra._resolve_vmid_for_device", return_value=101)
47
+ @patch("portacode.connection.handlers.proxmox_infra._get_node_from_config", return_value="pve")
48
+ @patch("portacode.connection.handlers.proxmox_infra._connect_proxmox")
49
+ @patch("portacode.connection.handlers.proxmox_infra._ensure_infra_configured", return_value={})
50
+ def test_restart_validates_child_then_stops_and_starts_once(
51
+ self, _config, connect, _node, _resolve, ensure_managed, stop, start, update, _snapshot
52
+ ):
53
+ stop.return_value = ({"exitstatus": "OK", "status": "stopped"}, 1.0)
54
+ start.return_value = ({"exitstatus": "OK", "status": "running"}, 2.0)
55
+ handler = RestartProxmoxContainerHandler(MagicMock(), {})
56
+
57
+ result = handler.execute({"child_device_id": "42"})
58
+
59
+ ensure_managed.assert_called_once_with(connect.return_value, "pve", 101, device_id="42")
60
+ stop.assert_called_once_with(connect.return_value, "pve", 101)
61
+ start.assert_called_once_with(connect.return_value, "pve", 101)
62
+ update.assert_called_once_with(101, {"status": "running"})
63
+ self.assertEqual(result["action"], "restart")
64
+ self.assertTrue(result["success"])
65
+
39
66
  def test_container_config_memory_is_always_mib_even_above_10000(self):
40
67
  self.assertEqual(
41
68
  _pick_container_ram_mib("lxc", {"memory": 32768}, {"maxmem": 34359738368}),
@@ -211,6 +238,50 @@ class ProxmoxInfraHandlerTests(TestCase):
211
238
  self.assertEqual(result["cleaned_vmids"], [])
212
239
  self.assertEqual(result["cleanup_errors"][0]["vmid"], "144")
213
240
 
241
+ @patch("portacode.connection.handlers.proxmox_infra._load_config", return_value={"node": "pve"})
242
+ def test_reconciliation_does_not_hold_state_lock_during_forwarding_cleanup(self, _config):
243
+ from portacode.connection.handlers import proxmox_infra
244
+
245
+ entered_cleanup = threading.Event()
246
+ release_cleanup = threading.Event()
247
+ state = {
248
+ "initialized": True, "base_summary": {}, "initial_totals": {},
249
+ "records": {"144": {"vmid": 144, "device_id": "42"}},
250
+ "pending": {}, "revision": 2,
251
+ }
252
+ refreshed = {
253
+ "inventory_updated_at": "now",
254
+ "missing_containers": [{"vmid": "144", "device_id": "42"}],
255
+ "total_ram_mib": 0, "total_disk_gib": 0, "total_cpu_share": 0,
256
+ }
257
+
258
+ def blocking_cleanup(_device_id, _rules):
259
+ entered_cleanup.set()
260
+ release_cleanup.wait(timeout=1)
261
+
262
+ with TemporaryDirectory() as root, patch(
263
+ "portacode.connection.handlers.proxmox_infra.CONTAINERS_DIR", Path(root)
264
+ ), patch(
265
+ "portacode.connection.handlers.proxmox_infra._MANAGED_CONTAINERS_STATE", state
266
+ ), patch(
267
+ "portacode.connection.handlers.proxmox_infra._build_full_container_summary",
268
+ return_value=refreshed,
269
+ ), patch(
270
+ "portacode.connection.handlers.cloudflare_forwarding.set_container_forwarding_rules",
271
+ side_effect=blocking_cleanup,
272
+ ):
273
+ worker = threading.Thread(target=reconcile_managed_containers_inventory)
274
+ worker.start()
275
+ self.assertTrue(entered_cleanup.wait(timeout=0.2))
276
+ acquired = proxmox_infra._MANAGED_CONTAINERS_STATE_LOCK.acquire(timeout=0.2)
277
+ if acquired:
278
+ proxmox_infra._MANAGED_CONTAINERS_STATE_LOCK.release()
279
+ release_cleanup.set()
280
+ worker.join(timeout=1)
281
+
282
+ self.assertTrue(acquired)
283
+ self.assertFalse(worker.is_alive())
284
+
214
285
  @patch("portacode.connection.handlers.proxmox_infra._load_config", return_value={"node": "pve"})
215
286
  def test_reconciliation_discards_scan_when_managed_state_changes(self, _config):
216
287
  state = {
@@ -74,6 +74,7 @@ from .handlers import (
74
74
  StartPortacodeServiceHandler,
75
75
  StartProxmoxContainerHandler,
76
76
  StopProxmoxContainerHandler,
77
+ RestartProxmoxContainerHandler,
77
78
  RemoveProxmoxContainerHandler,
78
79
  CloudflareTunnelSetupHandler,
79
80
  CloudflareForwardingHandler,
@@ -110,6 +111,7 @@ from portacode.tunneling.privileged import read_text, run, write_text
110
111
  logger = logging.getLogger(__name__)
111
112
  EXPOSED_SERVICES_MONITOR_PATH = "/etc/portacode/exposed_services.json"
112
113
  EXPOSED_SERVICES_MONITOR_INTERVAL_S = 2.0
114
+ SYSTEM_INFO_INTERVAL_S = 10.0
113
115
  EXPOSED_SERVICES_MISSING_SIGNATURE = "__missing__"
114
116
  EXPOSED_SERVICES_INVALID_SIGNATURE = "__invalid__"
115
117
  EXPOSED_SERVICES_ENV_PATH = Path("/etc/portacode/exposed_services.env")
@@ -595,6 +597,7 @@ class TerminalManager:
595
597
  self._command_registry.register(StartPortacodeServiceHandler)
596
598
  self._command_registry.register(StartProxmoxContainerHandler)
597
599
  self._command_registry.register(StopProxmoxContainerHandler)
600
+ self._command_registry.register(RestartProxmoxContainerHandler)
598
601
  self._command_registry.register(RemoveProxmoxContainerHandler)
599
602
  self._command_registry.register(RevertProxmoxInfraHandler)
600
603
  self._command_registry.register(CloudflareTunnelSetupHandler)
@@ -776,12 +779,14 @@ class TerminalManager:
776
779
  """Send system_info event every 10 seconds when clients are connected."""
777
780
  while True:
778
781
  try:
779
- await asyncio.sleep(10)
780
- if self._client_session_manager.has_interested_clients():
781
- from .handlers.system_handlers import SystemInfoHandler
782
- handler = SystemInfoHandler(self._control_channel, self._context)
783
- system_info = handler.execute({})
784
- await self._send_session_aware(system_info)
782
+ await asyncio.sleep(SYSTEM_INFO_INTERVAL_S)
783
+ if not self._client_session_manager.has_interested_clients():
784
+ continue
785
+ # CommandRegistry owns the single-flight invariant shared by
786
+ # periodic ticks, dashboard joins, and explicit probes.
787
+ await self._command_registry.dispatch("system_info", {}, None)
788
+ except asyncio.CancelledError:
789
+ raise
785
790
  except Exception as exc:
786
791
  logger.exception("Error in periodic system info: %s", exc)
787
792
  continue
@@ -30,6 +30,21 @@ class Registry:
30
30
  return self.handler if command == self.handler.command_name else None
31
31
 
32
32
 
33
+ class CommandRecordingHandler(SyncHandler):
34
+ def __init__(self, control_channel, session, command):
35
+ super().__init__(control_channel, session)
36
+ self._command = command
37
+ self.messages = []
38
+
39
+ @property
40
+ def command_name(self):
41
+ return self._command
42
+
43
+ def execute(self, message):
44
+ self.messages.append(message)
45
+ return {"event": "handled", "command": self._command}
46
+
47
+
33
48
  def request(request_id="ghr-1", target="42"):
34
49
  return {
35
50
  "type": GUEST_HOST_REQUEST,
@@ -76,11 +91,36 @@ class GuestHostTerminalTests(IsolatedAsyncioTestCase):
76
91
  await self.manager._handle_guest_host_request(envelope)
77
92
  self.assertEqual(self.manager._control_channel.sent, [])
78
93
 
79
- async def test_reserved_but_unimplemented_request_type_is_rejected(self):
94
+ async def test_enabled_request_without_registered_handler_is_rejected(self):
80
95
  envelope = request()
81
96
  envelope["command"] = "stop_proxmox_container"
82
97
  envelope["authorization"]["operation"] = "power.stop"
83
98
  await self.manager._handle_guest_host_request(envelope)
84
99
  ack, result = self.manager._control_channel.sent
85
- self.assertFalse(ack["data"]["accepted"])
86
- self.assertEqual(result["data"]["error_code"], "unsupported_request_type")
100
+ self.assertTrue(ack["data"]["accepted"])
101
+ self.assertEqual(result["data"]["error_code"], "handler_unavailable")
102
+
103
+ async def test_each_advertised_operation_delegates_to_existing_handler(self):
104
+ cases = (
105
+ ("start_proxmox_container", "power.start"),
106
+ ("stop_proxmox_container", "power.stop"),
107
+ ("restart_proxmox_container", "power.reboot"),
108
+ ("remove_proxmox_container", "delete"),
109
+ ("create_proxmox_container", "provision"),
110
+ )
111
+ for command, operation in cases:
112
+ with self.subTest(command=command):
113
+ handler = CommandRecordingHandler(self.manager._control_channel, {}, command)
114
+ self.manager._command_registry = Registry(handler)
115
+ self.manager._control_channel.sent.clear()
116
+ envelope = request(request_id=f"ghr-{command}")
117
+ envelope["command"] = command
118
+ envelope["authorization"]["operation"] = operation
119
+
120
+ await self.manager._handle_guest_host_request(envelope)
121
+
122
+ ack, result = self.manager._control_channel.sent
123
+ self.assertTrue(ack["data"]["accepted"])
124
+ self.assertTrue(result["data"]["success"])
125
+ self.assertEqual(len(handler.messages), 1)
126
+ self.assertEqual(handler.messages[0]["child_device_id"], "42")
@@ -0,0 +1,59 @@
1
+ import asyncio
2
+ from types import SimpleNamespace
3
+ from unittest.mock import AsyncMock
4
+
5
+ import pytest
6
+
7
+ from portacode.connection import terminal
8
+ from portacode.connection.handlers.registry import CommandRegistry
9
+
10
+
11
+ @pytest.mark.asyncio
12
+ async def test_registry_coalesces_concurrent_system_info_dispatches():
13
+ started = asyncio.Event()
14
+ release = asyncio.Event()
15
+ calls = 0
16
+
17
+ class BlockingHandler:
18
+ async def handle(self, _message, _reply_channel):
19
+ nonlocal calls
20
+ calls += 1
21
+ started.set()
22
+ await release.wait()
23
+
24
+ registry = CommandRegistry(AsyncMock(), {})
25
+ registry._handlers["system_info"] = BlockingHandler()
26
+
27
+ assert await registry.dispatch("system_info", {}, None)
28
+ await asyncio.wait_for(started.wait(), timeout=0.1)
29
+ assert await registry.dispatch("system_info", {}, None)
30
+ assert calls == 1
31
+
32
+ inflight = registry._inflight_dispatches["system_info"]
33
+ release.set()
34
+ await asyncio.wait_for(inflight, timeout=0.1)
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_periodic_system_info_uses_shared_registry(monkeypatch):
39
+ manager = object.__new__(terminal.TerminalManager)
40
+ manager._client_session_manager = SimpleNamespace(
41
+ has_interested_clients=lambda: True
42
+ )
43
+ manager._command_registry = SimpleNamespace(dispatch=AsyncMock(return_value=True))
44
+ monkeypatch.setattr(terminal, "SYSTEM_INFO_INTERVAL_S", 0.001)
45
+ periodic = asyncio.create_task(manager._periodic_system_info())
46
+
47
+ try:
48
+ async def wait_for_dispatch():
49
+ while not manager._command_registry.dispatch.await_count:
50
+ await asyncio.sleep(0.001)
51
+
52
+ await asyncio.wait_for(wait_for_dispatch(), timeout=0.1)
53
+ manager._command_registry.dispatch.assert_awaited_with(
54
+ "system_info", {}, None
55
+ )
56
+ finally:
57
+ periodic.cancel()
58
+ with pytest.raises(asyncio.CancelledError):
59
+ await periodic
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 2.0.0.dev0
3
+ Version: 2.0.2
4
4
  Summary: Device agent for the AI platform that builds, tests, deploys, and operates real software
5
5
  Home-page: https://portacode.com/
6
6
  Author: Meena Erian