portacode 1.5.17.dev4__tar.gz → 1.5.18.dev0__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 (443) hide show
  1. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/PKG-INFO +1 -1
  2. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/_version.py +2 -2
  3. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/codex_app_server.py +70 -11
  4. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/codex_handlers.py +96 -17
  5. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/git_manager.py +2 -0
  6. portacode-1.5.18.dev0/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
  7. portacode-1.5.18.dev0/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  8. portacode-1.5.18.dev0/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
  9. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tests/test_codex_app_server.py +30 -1
  10. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tests/test_codex_handlers.py +96 -0
  11. portacode-1.5.18.dev0/portacode/tests/test_codex_loopback_proxy.py +137 -0
  12. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode.egg-info/PKG-INFO +1 -1
  13. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode.egg-info/SOURCES.txt +4 -0
  14. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/.claude/agents/communication-manager.md +0 -0
  15. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/.claude/settings.local.json +0 -0
  16. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/.gitignore +0 -0
  17. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/.gitmodules +0 -0
  18. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/LICENSE +0 -0
  19. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/MANIFEST.in +0 -0
  20. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/Makefile +0 -0
  21. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/README.md +0 -0
  22. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/backup.sh +0 -0
  23. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/build_android.sh +0 -0
  24. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/connect.py +0 -0
  25. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/connect.sh +0 -0
  26. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docker-compose.yaml +0 -0
  27. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
  28. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/creative-team-brief-portacode.md +0 -0
  29. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/device-health-alerting-plan.md +0 -0
  30. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/devops-messaging-ab-tests.md +0 -0
  31. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/homepage-dashboard-positioning-fixes.md +0 -0
  32. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/images/device-transfer-button.png +0 -0
  33. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/images/device-transfer-modal.png +0 -0
  34. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/images/pair-device-button.png +0 -0
  35. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/images/pairing-request.png +0 -0
  36. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/images/student-workspace.png +0 -0
  37. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/docs/template-guide.html +0 -0
  38. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/README.md +0 -0
  39. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/simple_device/Dockerfile +0 -0
  40. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/simple_device/README.md +0 -0
  41. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
  42. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  43. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/simple_device/docker-compose.yaml +0 -0
  44. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/Dockerfile +0 -0
  45. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/README.md +0 -0
  46. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
  47. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  48. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
  49. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
  50. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
  51. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
  52. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
  53. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
  54. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
  55. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
  56. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
  57. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
  58. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
  59. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
  60. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
  61. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
  62. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
  63. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
  64. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
  65. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
  66. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
  67. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
  68. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
  69. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
  70. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
  71. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
  72. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
  73. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
  74. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
  75. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
  76. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  77. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
  78. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
  79. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
  80. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
  81. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
  82. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
  83. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
  84. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
  85. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
  86. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
  87. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
  88. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
  89. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
  90. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
  91. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
  92. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
  93. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
  94. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
  95. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
  96. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
  97. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
  98. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
  99. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
  100. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
  101. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
  102. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
  103. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
  104. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
  105. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
  106. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
  107. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
  108. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
  109. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
  110. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
  111. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
  112. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
  113. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
  114. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
  115. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
  116. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
  117. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
  118. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
  119. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
  120. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
  121. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
  122. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
  123. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
  124. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
  125. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
  126. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
  127. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
  128. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
  129. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
  130. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
  131. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
  132. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
  133. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
  134. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
  135. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
  136. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
  137. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
  138. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
  139. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
  140. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
  141. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
  142. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
  143. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
  144. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
  145. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
  146. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
  147. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
  148. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
  149. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
  150. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
  151. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
  152. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
  153. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
  154. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
  155. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
  156. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
  157. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
  158. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
  159. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
  160. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
  161. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
  162. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
  163. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
  164. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
  165. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
  166. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
  167. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
  168. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
  169. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
  170. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
  171. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
  172. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
  173. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
  174. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
  175. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
  176. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
  177. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
  178. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
  179. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
  180. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
  181. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
  182. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
  183. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
  184. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
  185. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
  186. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
  187. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
  188. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
  189. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
  190. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
  191. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
  192. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
  193. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
  194. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
  195. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
  196. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
  197. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
  198. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
  199. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
  200. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
  201. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
  202. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
  203. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
  204. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
  205. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
  206. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
  207. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
  208. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
  209. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
  210. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
  211. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
  212. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
  213. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
  214. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
  215. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
  216. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
  217. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
  218. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
  219. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
  220. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
  221. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
  222. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
  223. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
  224. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
  225. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
  226. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
  227. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
  228. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
  229. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
  230. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
  231. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
  232. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
  233. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
  234. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
  235. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
  236. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
  237. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
  238. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
  239. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
  240. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
  241. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
  242. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
  243. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
  244. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
  245. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
  246. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
  247. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
  248. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
  249. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
  250. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
  251. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
  252. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
  253. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
  254. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
  255. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
  256. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
  257. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
  258. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
  259. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
  260. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
  261. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/docker-compose.yaml +0 -0
  262. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/README.md +0 -0
  263. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
  264. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
  265. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
  266. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
  267. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
  268. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/manage.py +0 -0
  269. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
  270. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
  271. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
  272. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
  273. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
  274. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
  275. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
  276. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
  277. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
  278. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
  279. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
  280. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
  281. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/README.md +0 -0
  282. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/__init__.py +0 -0
  283. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/__main__.py +0 -0
  284. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/cli.py +0 -0
  285. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/codex_loopback_proxy.py +0 -0
  286. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/codex_prepare.py +0 -0
  287. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/codex_usage_limit.py +0 -0
  288. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/README.md +0 -0
  289. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/__init__.py +0 -0
  290. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/client.py +0 -0
  291. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/README.md +0 -0
  292. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
  293. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/__init__.py +0 -0
  294. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
  295. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/base.py +0 -0
  296. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/chunked_content.py +0 -0
  297. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
  298. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
  299. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/diff_handlers.py +0 -0
  300. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/file_handlers.py +0 -0
  301. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
  302. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  303. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/README.md +0 -0
  304. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/__init__.py +0 -0
  305. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  306. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/handlers.py +0 -0
  307. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/manager.py +0 -0
  308. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/models.py +0 -0
  309. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
  310. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state/utils.py +0 -0
  311. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/project_state_handlers.py +0 -0
  312. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/proxmox_infra.py +0 -0
  313. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/registry.py +0 -0
  314. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/runtime_user.py +0 -0
  315. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/session.py +0 -0
  316. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/system_handlers.py +0 -0
  317. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/tab_factory.py +0 -0
  318. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/terminal_handlers.py +0 -0
  319. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
  320. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
  321. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/test_runtime_user.py +0 -0
  322. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/handlers/update_handler.py +0 -0
  323. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/multiplex.py +0 -0
  324. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/terminal.py +0 -0
  325. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/test_webmin_proxy_config.py +0 -0
  326. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/connection/webmin_proxy_config.py +0 -0
  327. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/data.py +0 -0
  328. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/exit_codes.py +0 -0
  329. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/keypair.py +0 -0
  330. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/__init__.py +0 -0
  331. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  332. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/elinks +0 -0
  333. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/gio-open +0 -0
  334. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/gnome-open +0 -0
  335. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/gvfs-open +0 -0
  336. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/kde-open +0 -0
  337. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/kfmclient +0 -0
  338. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
  339. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
  340. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/links +0 -0
  341. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/links2 +0 -0
  342. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/lynx +0 -0
  343. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/mate-open +0 -0
  344. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/netsurf +0 -0
  345. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/sensible-browser +0 -0
  346. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/w3m +0 -0
  347. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/x-www-browser +0 -0
  348. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/link_capture/bin/xdg-open +0 -0
  349. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/logging_categories.py +0 -0
  350. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/pairing.py +0 -0
  351. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/restart.py +0 -0
  352. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/service.py +0 -0
  353. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/static/js/test-ntp-clock.html +0 -0
  354. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/static/js/utils/ntp-clock.js +0 -0
  355. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/test_runtime_paths.py +0 -0
  356. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/test_service.py +0 -0
  357. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/test_terminal_exposed_services.py +0 -0
  358. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tests/test_codex_prepare.py +0 -0
  359. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/__init__.py +0 -0
  360. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/cloudflared_login.py +0 -0
  361. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/ensure_cloudflared.py +0 -0
  362. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/ensure_pyyaml.py +0 -0
  363. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/forwarding_state.py +0 -0
  364. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/get_domain.py +0 -0
  365. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/privileged.py +0 -0
  366. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/service_install.py +0 -0
  367. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/tunneling/state.py +0 -0
  368. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/updater.py +0 -0
  369. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  370. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/utils/__init__.py +0 -0
  371. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/utils/diff_apply.py +0 -0
  372. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/utils/diff_renderer.py +0 -0
  373. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/utils/ntp_clock.py +0 -0
  374. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode/utils/runtime_paths.py +0 -0
  375. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode.egg-info/dependency_links.txt +0 -0
  376. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode.egg-info/entry_points.txt +0 -0
  377. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode.egg-info/requires.txt +0 -0
  378. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode.egg-info/top_level.txt +0 -0
  379. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/.gitignore +0 -0
  380. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/README.md +0 -0
  381. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/persistent_workspace/Dockerfile +0 -0
  382. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/persistent_workspace/README.md +0 -0
  383. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/persistent_workspace/entrypoint.sh +0 -0
  384. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/persistent_workspace/manage_classroom.py +0 -0
  385. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/temporary_workspace/Dockerfile +0 -0
  386. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/temporary_workspace/README.md +0 -0
  387. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/temporary_workspace/docker-compose.yaml +0 -0
  388. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
  389. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
  390. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/portacode_for_school/temporary_workspace/entrypoint.sh +0 -0
  391. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/pyproject.toml +0 -0
  392. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/restore.sh +0 -0
  393. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/run_tests.py +0 -0
  394. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/scripts/diagnose_codex_gateway.py +0 -0
  395. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/setup.cfg +0 -0
  396. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/setup.py +0 -0
  397. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test.sh +0 -0
  398. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/README.md +0 -0
  399. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/__init__.py +0 -0
  400. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_device_online.py +0 -0
  401. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_file_operations.py +0 -0
  402. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_git_status_ui.py +0 -0
  403. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_login_flow.py +0 -0
  404. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_navigate_testing_folder.py +0 -0
  405. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_play_store_screenshots.py +0 -0
  406. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_terminal_buffer_performance.py +0 -0
  407. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_terminal_interaction.py +0 -0
  408. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_terminal_loading_race_condition.py +0 -0
  409. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_modules/test_terminal_start.py +0 -0
  410. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_request_id.py +0 -0
  411. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/test_session_terminal_shutdown.py +0 -0
  412. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/.env.example +0 -0
  413. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/README.md +0 -0
  414. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/__init__.py +0 -0
  415. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/cli.py +0 -0
  416. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/__init__.py +0 -0
  417. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/base_test.py +0 -0
  418. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/cli_manager.py +0 -0
  419. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/hierarchical_runner.py +0 -0
  420. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/playwright_manager.py +0 -0
  421. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/runner.py +0 -0
  422. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/shared_cli_manager.py +0 -0
  423. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/core/test_discovery.py +0 -0
  424. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/testing_framework/requirements.txt +0 -0
  425. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
  426. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
  427. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/agent_context_management.md +0 -0
  428. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/django_server_time_sync.md +0 -0
  429. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/device_performance_degradation.md +0 -0
  430. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
  431. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/indefinite_resource_loading.md +0 -0
  432. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/portacode_service_silently_down.md +0 -0
  433. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/premature_terminal_exit.md +0 -0
  434. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/project_cpu_hotspots.md +0 -0
  435. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/terminals_exit_upon_starting.md +0 -0
  436. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/websocket_client_silently_dead.md +0 -0
  437. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
  438. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/todo/smartphone_terminal_input_frustrations.md +0 -0
  439. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/tools/generate_play_store_assets.py +0 -0
  440. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/tools/pairing_tester.py +0 -0
  441. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/tools/run_screenshot_suite.sh +0 -0
  442. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/tools/test_python_ntp_clock.py +0 -0
  443. {portacode-1.5.17.dev4 → portacode-1.5.18.dev0}/validate.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.5.17.dev4
3
+ Version: 1.5.18.dev0
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
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 = '1.5.17.dev4'
22
- __version_tuple__ = version_tuple = (1, 5, 17, 'dev4')
21
+ __version__ = version = '1.5.18.dev0'
22
+ __version_tuple__ = version_tuple = (1, 5, 18, 'dev0')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import shutil
9
10
  import uuid
10
11
  from pathlib import Path
@@ -32,6 +33,7 @@ class CodexAppServer:
32
33
  command: Optional[List[str]] = None,
33
34
  client_info: Optional[Dict[str, Any]] = None,
34
35
  on_notification: Optional[Callable[[str, Dict[str, Any]], Awaitable[None]]] = None,
36
+ on_unexpected_exit: Optional[Callable[[Optional[int]], Awaitable[None]]] = None,
35
37
  _process_factory: Optional[Callable[..., Awaitable[Any]]] = None,
36
38
  ) -> None:
37
39
  self.command = command or list(DEFAULT_CODEX_COMMAND)
@@ -43,6 +45,7 @@ class CodexAppServer:
43
45
  client_info = {"name": "portacode", "version": _portacode_version}
44
46
  self.client_info = client_info
45
47
  self.on_notification = on_notification
48
+ self.on_unexpected_exit = on_unexpected_exit
46
49
  self._process_factory = _process_factory or asyncio.create_subprocess_exec
47
50
 
48
51
  self._proc: Optional[asyncio.subprocess.Process] = None
@@ -74,12 +77,6 @@ class CodexAppServer:
74
77
 
75
78
  async def _spawn(self) -> None:
76
79
  """Spawn the subprocess and perform the JSON-RPC initialize handshake."""
77
- exe = shutil.which(self.command[0]) or self.command[0]
78
- if not shutil.which(self.command[0]):
79
- raise CodexAppServerError(
80
- f"{self.command[0]} not found on PATH. Run `portacode prepare codex` on this device."
81
- )
82
-
83
80
  # systemd services do not load shell profile env; merge /etc/portacode/codex.env.
84
81
  from portacode.codex_prepare import build_codex_subprocess_env, ensure_codex_home
85
82
  from portacode.connection.handlers.runtime_user import (
@@ -93,9 +90,16 @@ class CodexAppServer:
93
90
  # (e.g. bishoy), not as the root agent process.
94
91
  runtime_user = get_default_runtime_user()
95
92
  runtime_home = get_runtime_user_home()
93
+ exe = self._resolve_command_path(self.command[0], runtime_home)
94
+ if not exe:
95
+ raise CodexAppServerError(
96
+ f"{self.command[0]} not found for runtime user {runtime_user}. "
97
+ "Run `portacode prepare codex` on this device."
98
+ )
96
99
  child_env["HOME"] = runtime_home
97
100
  child_env["USER"] = runtime_user
98
101
  child_env["LOGNAME"] = runtime_user
102
+ self._prepend_executable_directory(child_env, exe)
99
103
  try:
100
104
  child_env["CODEX_HOME"] = str(ensure_codex_home())
101
105
  except Exception:
@@ -123,7 +127,7 @@ class CodexAppServer:
123
127
  "no_proxy",
124
128
  ]
125
129
  spawn_cmd = wrap_argv_for_user(
126
- self.command,
130
+ [exe, *self.command[1:]],
127
131
  runtime_user,
128
132
  preserve_env_names=preserve,
129
133
  login=False,
@@ -382,6 +386,11 @@ class CodexAppServer:
382
386
  )
383
387
  return
384
388
  LOGGER.warning("Codex app-server process exited (code %s); restarting", self._proc.returncode)
389
+ if self.on_unexpected_exit is not None:
390
+ try:
391
+ await self.on_unexpected_exit(self._proc.returncode)
392
+ except Exception:
393
+ LOGGER.exception("Codex app-server exit handler failed")
385
394
  await self._kill_locked()
386
395
  try:
387
396
  await self.start()
@@ -415,19 +424,69 @@ class CodexAppServer:
415
424
  return self._initialized.is_set()
416
425
 
417
426
  @staticmethod
418
- def get_binary_path() -> Optional[str]:
419
- return shutil.which("codex")
427
+ def get_binary_path(runtime_home: Optional[str] = None) -> Optional[str]:
428
+ """Find Codex for the service's configured runtime user.
429
+
430
+ A systemd agent normally has a deliberately small PATH. That should
431
+ not hide a Codex CLI installed by the runtime user through nvm or a
432
+ local npm prefix, which is common on developer machines.
433
+ """
434
+ from portacode.connection.handlers.runtime_user import get_runtime_user_home
435
+
436
+ return CodexAppServer._resolve_command_path(
437
+ "codex", runtime_home or get_runtime_user_home()
438
+ )
439
+
440
+ @staticmethod
441
+ def _resolve_command_path(command: str, runtime_home: Optional[str] = None) -> Optional[str]:
442
+ if not command:
443
+ return None
444
+ resolved = shutil.which(command)
445
+ if resolved:
446
+ return resolved
447
+
448
+ candidate = Path(command).expanduser()
449
+ if candidate.is_absolute() or candidate.parent != Path("."):
450
+ if candidate.is_file() and os.access(str(candidate), os.X_OK):
451
+ return str(candidate)
452
+ return None
453
+ if command not in {"codex", "codex.cmd"}:
454
+ return None
455
+
456
+ home = Path(runtime_home or Path.home()).expanduser()
457
+ candidates = [
458
+ home / ".local" / "bin" / command,
459
+ home / ".npm" / "bin" / command,
460
+ ]
461
+ nvm_root = home / ".nvm" / "versions" / "node"
462
+ if nvm_root.is_dir():
463
+ candidates.extend(sorted(nvm_root.glob("*/bin/" + command), reverse=True))
464
+ for candidate in candidates:
465
+ if candidate.is_file() and os.access(str(candidate), os.X_OK):
466
+ return str(candidate)
467
+ return None
468
+
469
+ @staticmethod
470
+ def _prepend_executable_directory(env: Dict[str, str], executable: str) -> None:
471
+ """Make the Node interpreter beside an npm launcher available to env."""
472
+ executable_dir = str(Path(executable).parent)
473
+ current_path = env.get("PATH") or os.defpath
474
+ entries = current_path.split(os.pathsep)
475
+ if executable_dir not in entries:
476
+ env["PATH"] = os.pathsep.join([executable_dir, current_path])
420
477
 
421
478
  @staticmethod
422
479
  def version() -> Optional[str]:
423
480
  import subprocess
424
481
 
425
- exe = shutil.which("codex")
482
+ exe = CodexAppServer.get_binary_path()
426
483
  if not exe:
427
484
  return None
428
485
  try:
486
+ env = dict(os.environ)
487
+ CodexAppServer._prepend_executable_directory(env, exe)
429
488
  result = subprocess.run(
430
- [exe, "--version"], capture_output=True, text=True, timeout=10
489
+ [exe, "--version"], capture_output=True, text=True, timeout=10, env=env
431
490
  )
432
491
  return result.stdout.strip() or None
433
492
  except Exception:
@@ -87,17 +87,23 @@ class _SessionAwareSender:
87
87
  self.control_channel = control_channel
88
88
  self.context = context
89
89
 
90
- async def send(self, payload: Dict[str, Any], project_id: Optional[str] = None) -> None:
90
+ async def send(
91
+ self,
92
+ payload: Dict[str, Any],
93
+ project_id: Optional[str] = None,
94
+ explicit_sessions: Optional[List[str]] = None,
95
+ ) -> None:
96
+ target_sessions = set(explicit_sessions or [])
91
97
  client_session_manager = self.context.get("client_session_manager")
92
98
  if client_session_manager and client_session_manager.has_interested_clients():
93
- target_sessions = client_session_manager.get_target_sessions(project_id)
94
- if target_sessions:
95
- payload = dict(payload)
96
- payload["client_sessions"] = target_sessions
99
+ target_sessions.update(client_session_manager.get_target_sessions(project_id))
97
100
  reply_channel = client_session_manager.get_reply_channel_for_compatibility()
98
101
  if reply_channel:
99
102
  payload = dict(payload)
100
103
  payload["reply_channel"] = reply_channel
104
+ if target_sessions:
105
+ payload = dict(payload)
106
+ payload["client_sessions"] = sorted(target_sessions)
101
107
 
102
108
  await self.control_channel.send(payload)
103
109
 
@@ -107,14 +113,46 @@ class CodexChatManager:
107
113
 
108
114
  def __init__(self, control_channel: Any, context: Dict[str, Any]) -> None:
109
115
  self.sender = _SessionAwareSender(control_channel, context)
110
- self.bridge = CodexAppServer(on_notification=self._on_notification)
116
+ self.bridge = CodexAppServer(
117
+ on_notification=self._on_notification,
118
+ on_unexpected_exit=self._on_bridge_exit,
119
+ )
111
120
  self._thread_project: Dict[str, str] = {}
112
121
  self._cwd_project: Dict[str, str] = {}
122
+ self._thread_active_turn: Dict[str, str] = {}
123
+ self._thread_sessions: Dict[str, set[str]] = {}
113
124
  # Survives page reloads via codex_status so the IDE can restore UI.
114
125
  self._prepare_running = False
115
126
  self._prepare_step: Optional[str] = None
116
127
  self._prepare_error: Optional[str] = None
117
128
 
129
+ async def _on_bridge_exit(self, returncode: Optional[int]) -> None:
130
+ """Turn an app-server crash into terminal events for waiting browsers."""
131
+ active = list(self._thread_active_turn.items())
132
+ self._thread_active_turn.clear()
133
+ for thread_id, turn_id in active:
134
+ project_id = self._thread_project.get(thread_id)
135
+ if not project_id:
136
+ continue
137
+ suffix = f" (exit code {returncode})" if returncode is not None else ""
138
+ params = {
139
+ "threadId": thread_id,
140
+ "turnId": turn_id,
141
+ "error": {
142
+ "message": f"The local Codex app-server stopped unexpectedly{suffix}.",
143
+ "additionalDetails": "Portacode is restarting it. Retry the message after it reconnects.",
144
+ },
145
+ }
146
+ await self.sender.send(
147
+ {
148
+ "event": "codex_event",
149
+ "notification": {"method": "error", "params": params},
150
+ "project_id": project_id,
151
+ },
152
+ project_id=project_id,
153
+ explicit_sessions=list(self._thread_sessions.get(thread_id, set())),
154
+ )
155
+
118
156
  async def _on_notification(self, method: str, params: Dict[str, Any]) -> None:
119
157
  # Attach usage-limit reset timestamp captured by the loopback proxy.
120
158
  if method == "error" or (
@@ -131,6 +169,13 @@ class CodexChatManager:
131
169
  params["turn"] = turn
132
170
 
133
171
  self._remember_thread_from_params(params)
172
+ thread_id = params.get("threadId") or (params.get("thread") or {}).get("id")
173
+ turn = params.get("turn") if isinstance(params.get("turn"), dict) else {}
174
+ turn_id = params.get("turnId") or turn.get("id")
175
+ if thread_id and method == "turn/started" and turn_id:
176
+ self._thread_active_turn[str(thread_id)] = str(turn_id)
177
+ elif thread_id and method == "turn/completed":
178
+ self._thread_active_turn.pop(str(thread_id), None)
134
179
  project_id = self._resolve_project_id(params)
135
180
  if not project_id:
136
181
  # Unresolved project_id would fan out to every open project tab.
@@ -144,7 +189,11 @@ class CodexChatManager:
144
189
  "notification": {"method": method, "params": params},
145
190
  "project_id": project_id,
146
191
  }
147
- await self.sender.send(event_payload, project_id=project_id)
192
+ await self.sender.send(
193
+ event_payload,
194
+ project_id=project_id,
195
+ explicit_sessions=list(self._thread_sessions.get(str(thread_id), set())) if thread_id else None,
196
+ )
148
197
 
149
198
  def _remember_thread_from_params(self, params: Dict[str, Any]) -> None:
150
199
  """Opportunistically map threadId → project when cwd is already known."""
@@ -175,6 +224,16 @@ class CodexChatManager:
175
224
  if cwd and project_id:
176
225
  self._cwd_project[cwd] = project_id
177
226
 
227
+ def subscribe_thread(self, thread_id: str, client_session: Optional[str]) -> None:
228
+ """Route future thread events to a tab even before session discovery catches up."""
229
+ if not thread_id or not client_session:
230
+ return
231
+ sessions = self._thread_sessions.setdefault(str(thread_id), set())
232
+ sessions.add(str(client_session))
233
+ # Avoid an unbounded collection of stale channel names after many reconnects.
234
+ while len(sessions) > 32:
235
+ sessions.pop()
236
+
178
237
  async def ensure_started(self) -> None:
179
238
  await self.bridge.start()
180
239
 
@@ -544,6 +603,7 @@ class CodexThreadStartHandler(CodexAsyncHandler):
544
603
  raise CodexAppServerError("thread/start did not return a thread id")
545
604
 
546
605
  manager.record_thread(thread_id, cwd, project_id or cwd)
606
+ manager.subscribe_thread(thread_id, message.get("source_client_session"))
547
607
 
548
608
  payload = {
549
609
  "event": "codex_thread_started",
@@ -572,14 +632,17 @@ class CodexThreadResumeHandler(CodexAsyncHandler):
572
632
  manager = _get_manager(self)
573
633
  await manager.ensure_started()
574
634
 
635
+ # thread/resume can emit notifications before its RPC response. Route
636
+ # those notifications to the requesting project from the outset.
637
+ if project_id:
638
+ manager.record_thread(thread_id, cwd, project_id)
639
+ manager.subscribe_thread(thread_id, message.get("source_client_session"))
640
+
575
641
  resume_result, items, active_turn = await _resume_thread_history(
576
642
  manager, thread_id=thread_id, cwd=cwd
577
643
  )
578
644
  thread = _thread_from_result(resume_result)
579
645
 
580
- if project_id:
581
- manager.record_thread(thread_id, cwd, project_id)
582
-
583
646
  payload = {
584
647
  "event": "codex_thread_resumed",
585
648
  "threadId": thread_id,
@@ -588,6 +651,9 @@ class CodexThreadResumeHandler(CodexAsyncHandler):
588
651
  }
589
652
  if active_turn:
590
653
  payload["activeTurn"] = active_turn
654
+ active_turn_id = active_turn.get("id") or active_turn.get("turnId")
655
+ if active_turn_id:
656
+ manager._thread_active_turn[str(thread_id)] = str(active_turn_id)
591
657
  if project_id:
592
658
  payload["project_id"] = project_id
593
659
  return payload
@@ -614,6 +680,15 @@ class CodexTurnStartHandler(CodexAsyncHandler):
614
680
  manager = _get_manager(self)
615
681
  await manager.ensure_started()
616
682
 
683
+ project_id = _project_id(message)
684
+ cwd = message.get("cwd")
685
+ # Codex may emit turn/started, retry, or terminal error notifications
686
+ # before the turn/start RPC response. Establish routing before the call
687
+ # so the browser cannot miss the event that clears its streaming state.
688
+ if project_id:
689
+ manager.record_thread(thread_id, cwd, project_id)
690
+ manager.subscribe_thread(thread_id, message.get("source_client_session"))
691
+
617
692
  params = {
618
693
  "threadId": thread_id,
619
694
  "input": _build_turn_input(text or "", attachments),
@@ -627,15 +702,15 @@ class CodexTurnStartHandler(CodexAsyncHandler):
627
702
  params["summary"] = message["summary"]
628
703
 
629
704
  result = await manager.bridge.call("turn/start", params)
630
- project_id = _project_id(message)
631
- cwd = message.get("cwd")
632
- if project_id:
633
- manager.record_thread(thread_id, cwd, project_id)
705
+ turn = _turn_from_result(result)
706
+ turn_id = turn.get("id") or turn.get("turnId")
707
+ if turn_id:
708
+ manager._thread_active_turn[str(thread_id)] = str(turn_id)
634
709
 
635
710
  payload = {
636
711
  "event": "codex_turn_started",
637
712
  "threadId": thread_id,
638
- "turn": _turn_from_result(result),
713
+ "turn": turn,
639
714
  }
640
715
  if project_id:
641
716
  payload["project_id"] = project_id
@@ -652,12 +727,16 @@ class CodexTurnInterruptHandler(CodexAsyncHandler):
652
727
  async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
653
728
  thread_id = message.get("threadId")
654
729
  turn_id = message.get("turnId")
655
- if not thread_id or not turn_id:
656
- raise ValueError("threadId and turnId are required")
730
+ if not thread_id:
731
+ raise ValueError("threadId is required")
657
732
 
658
733
  manager = _get_manager(self)
734
+ turn_id = turn_id or manager._thread_active_turn.get(str(thread_id))
735
+ if not turn_id:
736
+ raise ValueError("No active turn was found for this thread")
659
737
  await manager.ensure_started()
660
738
  await manager.bridge.call("turn/interrupt", {"threadId": thread_id, "turnId": turn_id})
739
+ manager._thread_active_turn.pop(str(thread_id), None)
661
740
 
662
741
  project_id = _project_id(message)
663
742
  payload = {"event": "codex_turn_interrupted", "threadId": thread_id, "turnId": turn_id}
@@ -5,6 +5,8 @@ including status checking, diff generation, file content retrieval, and Git comm
5
5
  like staging, unstaging, and reverting files.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  import asyncio
9
11
  import hashlib
10
12
  import logging
@@ -193,11 +193,40 @@ async def test_spawn_runs_as_runtime_user(tmp_path, monkeypatch):
193
193
  await bridge.start()
194
194
  assert seen
195
195
  assert seen[0][:5] == ("/usr/sbin/runuser", "-u", "bishoy", "--preserve-environment", "--")
196
- assert "codex" in seen[0]
196
+ assert "/usr/bin/codex" in seen[0]
197
197
  assert seen_kwargs[0].get("cwd") == str(home)
198
198
  await bridge.stop()
199
199
 
200
200
 
201
+ @pytest.mark.asyncio
202
+ async def test_spawn_finds_nvm_codex_when_service_path_does_not(tmp_path, monkeypatch):
203
+ """A systemd PATH must not hide a runtime user's nvm-installed Codex."""
204
+ home = tmp_path / "home"
205
+ codex_bin = home / ".nvm" / "versions" / "node" / "v24.4.1" / "bin"
206
+ codex_bin.mkdir(parents=True)
207
+ codex = codex_bin / "codex"
208
+ codex.write_text("#!/usr/bin/env node\n", encoding="utf-8")
209
+ codex.chmod(0o755)
210
+ monkeypatch.setenv("PATH", "/usr/bin:/bin")
211
+ monkeypatch.setattr(
212
+ "portacode.connection.handlers.runtime_user.get_default_runtime_user",
213
+ lambda message=None: "menas",
214
+ )
215
+ monkeypatch.setattr(
216
+ "portacode.connection.handlers.runtime_user.get_runtime_user_home",
217
+ lambda message=None: str(home),
218
+ )
219
+ monkeypatch.setattr("portacode.codex_prepare.ensure_codex_home", lambda: home / ".codex")
220
+
221
+ proc = FakeProcess([{"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "v2"}}])
222
+ bridge = CodexAppServer(_process_factory=_factory_for(proc))
223
+ await bridge.start()
224
+
225
+ assert CodexAppServer.get_binary_path(str(home)) == str(codex)
226
+ assert proc.spawn_kwargs["env"]["PATH"].split(":")[0] == str(codex_bin)
227
+ await bridge.stop()
228
+
229
+
201
230
  def test_build_codex_subprocess_env_reads_managed_file(tmp_path):
202
231
  from portacode.codex_prepare import build_codex_subprocess_env
203
232
 
@@ -330,6 +330,47 @@ async def test_codex_turn_interrupt():
330
330
  assert manager.bridge.calls[-1] == ("turn/interrupt", {"threadId": "th-1", "turnId": "turn-1"})
331
331
 
332
332
 
333
+ @pytest.mark.asyncio
334
+ async def test_codex_turn_interrupt_resolves_active_turn_when_browser_missed_id():
335
+ channel = DummyControlChannel()
336
+ context = _context()
337
+ manager = CodexChatManager(channel, context)
338
+ manager.bridge = FakeBridge()
339
+ manager._thread_active_turn["th-1"] = "turn-active"
340
+ context["codex_manager"] = manager
341
+
342
+ handler = CodexTurnInterruptHandler(channel, context)
343
+ await handler.handle(
344
+ {"cmd": "codex_turn_interrupt", "project_id": "p-1", "threadId": "th-1"},
345
+ reply_channel="rc-1",
346
+ )
347
+
348
+ assert manager.bridge.calls[-1] == (
349
+ "turn/interrupt",
350
+ {"threadId": "th-1", "turnId": "turn-active"},
351
+ )
352
+ assert "th-1" not in manager._thread_active_turn
353
+
354
+
355
+ @pytest.mark.asyncio
356
+ async def test_codex_bridge_exit_terminates_active_browser_turn():
357
+ channel = DummyControlChannel()
358
+ context = _context()
359
+ manager = CodexChatManager(channel, context)
360
+ manager.record_thread("th-1", "/tmp/proj", "p-1")
361
+ manager._thread_active_turn["th-1"] = "turn-1"
362
+
363
+ await manager._on_bridge_exit(137)
364
+
365
+ payload = channel.sent[-1]
366
+ assert payload["event"] == "codex_event"
367
+ assert payload["project_id"] == "p-1"
368
+ assert payload["notification"]["method"] == "error"
369
+ assert payload["notification"]["params"]["turnId"] == "turn-1"
370
+ assert "exit code 137" in payload["notification"]["params"]["error"]["message"]
371
+ assert not manager._thread_active_turn
372
+
373
+
333
374
  @pytest.mark.asyncio
334
375
  async def test_notification_forwarding_targets_project():
335
376
  channel = DummyControlChannel()
@@ -346,6 +387,22 @@ async def test_notification_forwarding_targets_project():
346
387
  assert payload["client_sessions"] == ["sess-1"]
347
388
 
348
389
 
390
+ @pytest.mark.asyncio
391
+ async def test_resumed_thread_events_include_new_tab_before_session_registry_updates():
392
+ channel = DummyControlChannel()
393
+ context = _context()
394
+ manager = CodexChatManager(channel, context)
395
+ manager.record_thread("th-1", "/tmp/proj", "p-1")
396
+ manager.subscribe_thread("th-1", "new-tab-session")
397
+
398
+ await manager._on_notification(
399
+ "item/agentMessage/delta",
400
+ {"threadId": "th-1", "delta": "live"},
401
+ )
402
+
403
+ assert channel.sent[-1]["client_sessions"] == ["new-tab-session", "sess-1"]
404
+
405
+
349
406
  @pytest.mark.asyncio
350
407
  async def test_notification_without_project_mapping_is_dropped():
351
408
  channel = DummyControlChannel()
@@ -378,6 +435,45 @@ async def test_codex_turn_start_records_thread_mapping():
378
435
  assert manager._cwd_project["/tmp/proj"] == "p-1"
379
436
 
380
437
 
438
+ @pytest.mark.asyncio
439
+ async def test_codex_turn_start_routes_notification_before_rpc_response():
440
+ channel = DummyControlChannel()
441
+ context = _context()
442
+ manager = CodexChatManager(channel, context)
443
+ manager.bridge = FakeBridge()
444
+ context["codex_manager"] = manager
445
+
446
+ async def call(method: str, params: dict, timeout: float = 60.0):
447
+ assert method == "turn/start"
448
+ await manager._on_notification(
449
+ "error",
450
+ {
451
+ "threadId": "th-9",
452
+ "turnId": "turn-1",
453
+ "willRetry": False,
454
+ "error": {"message": "request failed"},
455
+ },
456
+ )
457
+ return {"turn": {"id": "turn-1"}}
458
+
459
+ manager.bridge.call = call # type: ignore[method-assign]
460
+ handler = CodexTurnStartHandler(channel, context)
461
+ await handler.handle(
462
+ {
463
+ "cmd": "codex_turn_start",
464
+ "project_id": "p-1",
465
+ "threadId": "th-9",
466
+ "cwd": "/tmp/proj",
467
+ "text": "hi",
468
+ },
469
+ reply_channel="rc-1",
470
+ )
471
+
472
+ notification = next(item for item in channel.sent if item.get("event") == "codex_event")
473
+ assert notification["project_id"] == "p-1"
474
+ assert notification["notification"]["method"] == "error"
475
+
476
+
381
477
  @pytest.mark.asyncio
382
478
  async def test_codex_thread_resume_includes_active_turn():
383
479
  channel = DummyControlChannel()