portacode 1.5.21.dev0__tar.gz → 1.5.21.dev2__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 (447) hide show
  1. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/PKG-INFO +1 -1
  2. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/_version.py +2 -2
  3. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/codex_loopback_proxy.py +48 -6
  4. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +10 -5
  5. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/resumable_transfer_handlers.py +100 -17
  6. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_loopback_proxy.py +28 -0
  7. portacode-1.5.21.dev2/portacode/tests/test_resumable_transfer_handlers.py +256 -0
  8. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/PKG-INFO +1 -1
  9. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/SOURCES.txt +1 -0
  10. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_play_store_screenshots.py +149 -18
  11. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/cli.py +15 -3
  12. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/hierarchical_runner.py +12 -9
  13. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/runner.py +16 -8
  14. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/run_screenshot_suite.sh +51 -11
  15. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.claude/agents/communication-manager.md +0 -0
  16. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.claude/settings.local.json +0 -0
  17. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.gitignore +0 -0
  18. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.gitmodules +0 -0
  19. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/LICENSE +0 -0
  20. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/MANIFEST.in +0 -0
  21. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/Makefile +0 -0
  22. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/README.md +0 -0
  23. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/build_android.sh +0 -0
  24. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/connect.py +0 -0
  25. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/connect.sh +0 -0
  26. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
  27. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/creative-team-brief-portacode.md +0 -0
  28. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/device-health-alerting-plan.md +0 -0
  29. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/devops-messaging-ab-tests.md +0 -0
  30. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/homepage-dashboard-positioning-fixes.md +0 -0
  31. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/device-transfer-button.png +0 -0
  32. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/device-transfer-modal.png +0 -0
  33. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/pair-device-button.png +0 -0
  34. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/pairing-request.png +0 -0
  35. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/student-workspace.png +0 -0
  36. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/template-guide.html +0 -0
  37. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/README.md +0 -0
  38. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/Dockerfile +0 -0
  39. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/README.md +0 -0
  40. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
  41. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  42. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/docker-compose.yaml +0 -0
  43. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/Dockerfile +0 -0
  44. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/README.md +0 -0
  45. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
  46. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
  47. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
  48. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
  49. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
  50. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
  51. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
  52. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
  53. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
  54. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
  55. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
  56. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
  57. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
  58. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
  59. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
  60. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
  61. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
  62. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
  63. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
  64. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
  65. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
  66. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
  67. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
  68. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
  69. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
  70. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
  71. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
  72. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
  73. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
  74. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
  75. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  76. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
  77. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
  78. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
  79. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
  80. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
  81. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
  82. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
  83. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
  84. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
  85. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
  86. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
  87. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
  88. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
  89. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
  90. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
  91. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
  92. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
  93. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
  94. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
  95. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
  96. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
  97. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
  98. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
  99. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
  100. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
  101. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
  102. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
  103. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
  104. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
  105. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
  106. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
  107. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
  108. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
  109. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
  110. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
  111. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
  112. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
  113. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
  114. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
  115. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
  116. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
  117. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
  118. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
  119. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
  120. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
  121. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
  122. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
  123. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
  124. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
  125. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
  126. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
  127. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
  128. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
  129. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
  130. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
  131. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
  132. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
  133. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
  134. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
  135. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
  136. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
  137. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
  138. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
  139. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
  140. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
  141. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
  142. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
  143. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
  144. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
  145. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
  146. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
  147. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
  148. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
  149. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
  150. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
  151. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
  152. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
  153. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
  154. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
  155. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
  156. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
  157. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
  158. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
  159. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
  160. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
  161. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
  162. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
  163. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
  164. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
  165. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
  166. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
  167. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
  168. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
  169. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
  170. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
  171. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
  172. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
  173. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
  174. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
  175. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
  176. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
  177. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
  178. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
  179. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
  180. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
  181. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
  182. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
  183. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
  184. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
  185. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
  186. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
  187. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
  188. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
  189. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
  190. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
  191. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
  192. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
  193. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
  194. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
  195. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
  196. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
  197. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
  198. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
  199. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
  200. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
  201. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
  202. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
  203. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
  204. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
  205. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
  206. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
  207. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
  208. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
  209. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
  210. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
  211. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
  212. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
  213. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
  214. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
  215. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
  216. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
  217. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
  218. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
  219. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
  220. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
  221. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
  222. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
  223. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
  224. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
  225. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
  226. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
  227. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
  228. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
  229. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
  230. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
  231. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
  232. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
  233. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
  234. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
  235. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
  236. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
  237. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
  238. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
  239. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
  240. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
  241. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
  242. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
  243. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
  244. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
  245. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
  246. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
  247. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
  248. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
  249. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
  250. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
  251. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
  252. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
  253. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
  254. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
  255. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
  256. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
  257. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
  258. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
  259. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
  260. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/docker-compose.yaml +0 -0
  261. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/README.md +0 -0
  262. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
  263. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
  264. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
  265. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
  266. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
  267. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/manage.py +0 -0
  268. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
  269. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
  270. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
  271. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
  272. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
  273. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
  274. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
  275. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
  276. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
  277. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
  278. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
  279. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
  280. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/README.md +0 -0
  281. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/__init__.py +0 -0
  282. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/__main__.py +0 -0
  283. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/cli.py +0 -0
  284. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/codex_prepare.py +0 -0
  285. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/codex_usage_limit.py +0 -0
  286. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/README.md +0 -0
  287. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/__init__.py +0 -0
  288. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/client.py +0 -0
  289. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/codex_app_server.py +0 -0
  290. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/README.md +0 -0
  291. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/__init__.py +0 -0
  292. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
  293. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/base.py +0 -0
  294. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/chunked_content.py +0 -0
  295. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
  296. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
  297. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/codex_handlers.py +0 -0
  298. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/diff_handlers.py +0 -0
  299. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/file_handlers.py +0 -0
  300. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
  301. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
  302. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/README.md +0 -0
  303. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/__init__.py +0 -0
  304. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
  305. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/git_manager.py +0 -0
  306. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/handlers.py +0 -0
  307. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/manager.py +0 -0
  308. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/models.py +0 -0
  309. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
  310. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/utils.py +0 -0
  311. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state_handlers.py +0 -0
  312. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/proxmox_infra.py +0 -0
  313. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/registry.py +0 -0
  314. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/runtime_user.py +0 -0
  315. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/session.py +0 -0
  316. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/system_handlers.py +0 -0
  317. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/tab_factory.py +0 -0
  318. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/terminal_handlers.py +0 -0
  319. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
  320. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_file_move_copy.py +0 -0
  321. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
  322. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_runtime_user.py +0 -0
  323. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/update_handler.py +0 -0
  324. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/multiplex.py +0 -0
  325. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/terminal.py +0 -0
  326. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/test_webmin_proxy_config.py +0 -0
  327. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/webmin_proxy_config.py +0 -0
  328. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/data.py +0 -0
  329. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/exit_codes.py +0 -0
  330. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/github_credential.py +0 -0
  331. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/keypair.py +0 -0
  332. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/__init__.py +0 -0
  333. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
  334. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
  335. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
  336. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
  337. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/elinks +0 -0
  338. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/gio-open +0 -0
  339. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/gnome-open +0 -0
  340. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/gvfs-open +0 -0
  341. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/kde-open +0 -0
  342. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/kfmclient +0 -0
  343. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
  344. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
  345. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/links +0 -0
  346. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/links2 +0 -0
  347. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/lynx +0 -0
  348. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/mate-open +0 -0
  349. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/netsurf +0 -0
  350. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/sensible-browser +0 -0
  351. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/w3m +0 -0
  352. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/x-www-browser +0 -0
  353. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/xdg-open +0 -0
  354. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/logging_categories.py +0 -0
  355. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/pairing.py +0 -0
  356. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/restart.py +0 -0
  357. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/service.py +0 -0
  358. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/singleton.py +0 -0
  359. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/static/js/test-ntp-clock.html +0 -0
  360. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/static/js/utils/ntp-clock.js +0 -0
  361. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_runtime_paths.py +0 -0
  362. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_service.py +0 -0
  363. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_singleton.py +0 -0
  364. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_terminal_exposed_services.py +0 -0
  365. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_app_server.py +0 -0
  366. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_handlers.py +0 -0
  367. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_prepare.py +0 -0
  368. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_github_credential.py +0 -0
  369. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/__init__.py +0 -0
  370. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/cloudflared_login.py +0 -0
  371. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/ensure_cloudflared.py +0 -0
  372. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/ensure_pyyaml.py +0 -0
  373. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/forwarding_state.py +0 -0
  374. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/get_domain.py +0 -0
  375. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/privileged.py +0 -0
  376. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/service_install.py +0 -0
  377. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/state.py +0 -0
  378. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/updater.py +0 -0
  379. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
  380. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/__init__.py +0 -0
  381. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/diff_apply.py +0 -0
  382. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/diff_renderer.py +0 -0
  383. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/ntp_clock.py +0 -0
  384. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/runtime_paths.py +0 -0
  385. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/dependency_links.txt +0 -0
  386. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/entry_points.txt +0 -0
  387. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/requires.txt +0 -0
  388. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/top_level.txt +0 -0
  389. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/.gitignore +0 -0
  390. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/README.md +0 -0
  391. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/Dockerfile +0 -0
  392. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/README.md +0 -0
  393. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/entrypoint.sh +0 -0
  394. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/manage_classroom.py +0 -0
  395. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/Dockerfile +0 -0
  396. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/README.md +0 -0
  397. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/docker-compose.yaml +0 -0
  398. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
  399. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
  400. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/entrypoint.sh +0 -0
  401. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/pyproject.toml +0 -0
  402. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/run_tests.py +0 -0
  403. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/scripts/diagnose_codex_gateway.py +0 -0
  404. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/setup.cfg +0 -0
  405. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/setup.py +0 -0
  406. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test.sh +0 -0
  407. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/README.md +0 -0
  408. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/__init__.py +0 -0
  409. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_device_online.py +0 -0
  410. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_file_operations.py +0 -0
  411. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_git_status_ui.py +0 -0
  412. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_login_flow.py +0 -0
  413. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_navigate_testing_folder.py +0 -0
  414. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_buffer_performance.py +0 -0
  415. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_interaction.py +0 -0
  416. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_loading_race_condition.py +0 -0
  417. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_start.py +0 -0
  418. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_request_id.py +0 -0
  419. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_session_terminal_shutdown.py +0 -0
  420. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/.env.example +0 -0
  421. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/README.md +0 -0
  422. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/__init__.py +0 -0
  423. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/__init__.py +0 -0
  424. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/base_test.py +0 -0
  425. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/cli_manager.py +0 -0
  426. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/playwright_manager.py +0 -0
  427. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/shared_cli_manager.py +0 -0
  428. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/test_discovery.py +0 -0
  429. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/requirements.txt +0 -0
  430. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
  431. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
  432. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/agent_context_management.md +0 -0
  433. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/django_server_time_sync.md +0 -0
  434. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/device_performance_degradation.md +0 -0
  435. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
  436. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/indefinite_resource_loading.md +0 -0
  437. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/portacode_service_silently_down.md +0 -0
  438. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/premature_terminal_exit.md +0 -0
  439. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/project_cpu_hotspots.md +0 -0
  440. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/terminals_exit_upon_starting.md +0 -0
  441. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/websocket_client_silently_dead.md +0 -0
  442. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
  443. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/smartphone_terminal_input_frustrations.md +0 -0
  444. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/generate_play_store_assets.py +0 -0
  445. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/pairing_tester.py +0 -0
  446. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/test_python_ntp_clock.py +0 -0
  447. {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/validate.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.5.21.dev0
3
+ Version: 1.5.21.dev2
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.21.dev0'
22
- __version_tuple__ = version_tuple = (1, 5, 21, 'dev0')
21
+ __version__ = version = '1.5.21.dev2'
22
+ __version_tuple__ = version_tuple = (1, 5, 21, 'dev2')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -36,7 +36,7 @@ DEFAULT_GATEWAY_URL = "https://codexapi.portacode.com/v1"
36
36
  MAX_REQUEST_BYTES = 32 * 1024 * 1024
37
37
  MAX_FORWARD_REQUEST_BYTES = 10 * 1024 * 1024
38
38
  MAX_TOOL_OUTPUT_BYTES = 64 * 1024
39
- ALLOWED_PATHS = {"/health", "/v1/models", "/v1/responses"}
39
+ ALLOWED_PATHS = {"/health", "/v1/models", "/v1/responses", "/v1/responses/compact"}
40
40
  MAX_CONCURRENT_UPSTREAM = 8
41
41
 
42
42
  # Streaming read can idle between SSE chunks; connect/write stay bounded.
@@ -117,9 +117,51 @@ def sanitize_responses_request(body: bytes) -> tuple[bytes, int, int]:
117
117
  part["text"] = cropped
118
118
  changed += 1
119
119
  omitted_total += omitted
120
- if not changed:
121
- return body, 0, 0
122
- return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8"), changed, omitted_total
120
+ encoded = (
121
+ json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
122
+ if changed
123
+ else body
124
+ )
125
+ if len(encoded) <= MAX_FORWARD_REQUEST_BYTES:
126
+ return encoded, changed, omitted_total
127
+
128
+ # Image-producing tools return screenshots as inline data URLs. A long
129
+ # session can therefore be under the model token limit while its JSON body
130
+ # is many megabytes too large to forward. Remove the oldest inline images
131
+ # first, retaining newer screenshots whenever that is sufficient. Text
132
+ # alongside each screenshot remains available to compaction/inference.
133
+ image_parts: list[dict] = []
134
+ for item in payload["input"]:
135
+ if not isinstance(item, dict) or item.get("type") not in _TOOL_OUTPUT_TYPES:
136
+ continue
137
+ value = item.get("output") or item.get("content")
138
+ if not isinstance(value, list):
139
+ continue
140
+ for part in value:
141
+ if (
142
+ isinstance(part, dict)
143
+ and part.get("type") in {"input_image", "image_url"}
144
+ and isinstance(part.get("image_url"), str)
145
+ and part["image_url"].startswith("data:image/")
146
+ ):
147
+ image_parts.append(part)
148
+
149
+ for part in image_parts:
150
+ if len(encoded) <= MAX_FORWARD_REQUEST_BYTES:
151
+ break
152
+ original_bytes = len(part["image_url"].encode("utf-8"))
153
+ part.clear()
154
+ part.update(
155
+ {
156
+ "type": "input_text",
157
+ "text": "[Portacode omitted an older inline tool screenshot to keep the request within the forwarding limit.]",
158
+ }
159
+ )
160
+ changed += 1
161
+ omitted_total += original_bytes
162
+ encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
163
+
164
+ return encoded, changed, omitted_total
123
165
 
124
166
  FORWARD_RESPONSE_HEADERS = frozenset(
125
167
  {
@@ -327,7 +369,7 @@ class CodexLoopbackProxy:
327
369
  if method == "GET" and path != "/v1/models":
328
370
  await self._write_error(writer, 405, "Method not allowed")
329
371
  return
330
- if method == "POST" and path != "/v1/responses":
372
+ if method == "POST" and path not in {"/v1/responses", "/v1/responses/compact"}:
331
373
  await self._write_error(writer, 405, "Method not allowed")
332
374
  return
333
375
  await self._forward(writer, method, path, headers, body, req_id=req_id)
@@ -451,7 +493,7 @@ class CodexLoopbackProxy:
451
493
  *,
452
494
  req_id: str,
453
495
  ) -> None:
454
- if method == "POST" and path == "/v1/responses" and body:
496
+ if method == "POST" and path in {"/v1/responses", "/v1/responses/compact"} and body:
455
497
  body, cropped_fields, omitted_bytes = sanitize_responses_request(body)
456
498
  if cropped_fields:
457
499
  LOGGER.warning(
@@ -830,10 +830,12 @@ its SHA-256 hash and binds it to one `transfer_id`, role, and path. Possession o
830
830
  a transfer ID alone grants no access.
831
831
 
832
832
  Folders are packaged once as an uncompressed PAX tar archive so subsequent
833
- chunk reads remain byte-for-byte stable. Destination extraction rejects path
834
- traversal, escaping links, and special device files. Modes, timestamps, numeric
835
- ownership, symlinks, and hard links are retained when the destination operating
836
- system and process privileges allow it.
833
+ chunk reads remain byte-for-byte stable. Source preparation and destination
834
+ extraction reject symbolic/hard links, special device files, path traversal,
835
+ and archives with more than the configured entry limit. Modes, timestamps, and
836
+ numeric ownership are retained when the destination operating system and
837
+ process privileges allow it. Links are deliberately rejected because chained
838
+ archive links cannot be extracted safely by legacy Python runtimes.
837
839
 
838
840
  ### `transfer_prepare`
839
841
 
@@ -851,7 +853,10 @@ Destination preparation additionally requires the source's `payload_size`,
851
853
  `entry_count`, `metadata`, and `archive_format`. It refuses the operation when
852
854
  free space cannot accommodate the incoming payload, simultaneous extraction,
853
855
  destination filesystem entry overhead, and a safety reserve. `overwrite`
854
- defaults to false.
856
+ defaults to false. Incoming bytes and extraction are staged on the destination
857
+ filesystem, so the final atomic rename also works when the Portacode cache and
858
+ destination are on different mounts. Payload and entry ceilings default to 10
859
+ GiB and 100,000 entries and are configurable by the device operator.
855
860
 
856
861
  Returns `transfer_prepared` with sizes, storage estimates, and progress.
857
862
 
@@ -29,6 +29,8 @@ MIN_CHUNK_SIZE = 32 * 1024
29
29
  MAX_CHUNK_SIZE = 1024 * 1024
30
30
  TRANSFER_TTL_SECONDS = 24 * 60 * 60
31
31
  STORAGE_SAFETY_BYTES = 16 * 1024 * 1024
32
+ MAX_TRANSFER_PAYLOAD_BYTES = int(os.environ.get("PORTACODE_TRANSFER_MAX_PAYLOAD_BYTES", 10 * 1024 * 1024 * 1024))
33
+ MAX_TRANSFER_ENTRIES = int(os.environ.get("PORTACODE_TRANSFER_MAX_ENTRIES", 100_000))
32
34
 
33
35
 
34
36
  def _cache_root() -> Path:
@@ -134,15 +136,20 @@ def _folder_metrics(path: Path) -> Dict[str, int]:
134
136
  entries = 1
135
137
  for root, directories, files in os.walk(path, followlinks=False):
136
138
  entries += len(directories) + len(files)
139
+ for name in directories:
140
+ item = Path(root) / name
141
+ info = item.lstat()
142
+ if stat.S_ISLNK(info.st_mode):
143
+ raise ValueError(f"Folder transfers do not support symbolic links: {item}")
137
144
  for name in files:
138
145
  item = Path(root) / name
139
- try:
140
- info = item.lstat()
141
- except OSError:
142
- continue
143
- if stat.S_ISREG(info.st_mode):
144
- logical += info.st_size
145
- allocated += max(info.st_blocks * 512, info.st_size)
146
+ info = item.lstat()
147
+ if stat.S_ISLNK(info.st_mode):
148
+ raise ValueError(f"Folder transfers do not support symbolic links: {item}")
149
+ if not stat.S_ISREG(info.st_mode):
150
+ raise ValueError(f"Folder transfers do not support special files: {item}")
151
+ logical += info.st_size
152
+ allocated += max(info.st_blocks * 512, info.st_size)
146
153
  return {"expanded_size": logical, "expanded_storage_bytes": allocated, "entry_count": entries}
147
154
 
148
155
 
@@ -184,11 +191,37 @@ def _cleanup_expired() -> None:
184
191
  manifest = json.loads((child / "manifest.json").read_text(encoding="utf-8"))
185
192
  expired = float(manifest.get("expires_at") or 0) <= now
186
193
  except Exception:
194
+ manifest = None
187
195
  expired = child.stat().st_mtime < now - TRANSFER_TTL_SECONDS
188
196
  if expired:
197
+ if manifest:
198
+ _cleanup_destination_staging(manifest)
189
199
  shutil.rmtree(child, ignore_errors=True)
190
200
 
191
201
 
202
+ def _cleanup_destination_staging(manifest: Dict[str, Any]) -> None:
203
+ """Remove only UUID-bound staging names outside the cache directory."""
204
+ transfer_id = str(manifest.get("transfer_id") or "")
205
+ expected = {
206
+ f".portacode-incoming-{transfer_id}.part": "file",
207
+ f".portacode-staged-{transfer_id}": "file",
208
+ f".portacode-extract-{transfer_id}": "directory",
209
+ }
210
+ for key in ("partial_path", "staged_path", "extraction_path"):
211
+ raw = manifest.get(key)
212
+ if not raw:
213
+ continue
214
+ path = Path(str(raw))
215
+ kind = expected.get(path.name)
216
+ try:
217
+ if kind == "directory":
218
+ shutil.rmtree(path, ignore_errors=True)
219
+ elif kind == "file":
220
+ path.unlink(missing_ok=True)
221
+ except OSError:
222
+ pass
223
+
224
+
192
225
  def _prepare_source(message: Dict[str, Any]) -> Dict[str, Any]:
193
226
  transfer_id = _transfer_id(message.get("transfer_id"))
194
227
  token = _authorization_token(message.get("authorization_token"))
@@ -212,9 +245,18 @@ def _prepare_source(message: Dict[str, Any]) -> Dict[str, Any]:
212
245
  metrics = {"expanded_size": source.stat().st_size, "expanded_storage_bytes": source.stat().st_size, "entry_count": 1}
213
246
  payload = source
214
247
  archive_format = None
248
+ if kind == "file" and source.stat().st_size > MAX_TRANSFER_PAYLOAD_BYTES:
249
+ shutil.rmtree(job_dir, ignore_errors=True)
250
+ raise ValueError(f"Transfer payload exceeds the {MAX_TRANSFER_PAYLOAD_BYTES} byte limit")
215
251
  if kind == "folder":
216
252
  metrics = _folder_metrics(source)
253
+ if metrics["entry_count"] > MAX_TRANSFER_ENTRIES:
254
+ shutil.rmtree(job_dir, ignore_errors=True)
255
+ raise ValueError(f"Folder exceeds the {MAX_TRANSFER_ENTRIES} entry limit")
217
256
  estimate = _estimated_tar_bytes(metrics)
257
+ if estimate > MAX_TRANSFER_PAYLOAD_BYTES:
258
+ shutil.rmtree(job_dir, ignore_errors=True)
259
+ raise ValueError(f"Estimated folder archive exceeds the {MAX_TRANSFER_PAYLOAD_BYTES} byte limit")
218
260
  available = _free_bytes(job_dir)
219
261
  required = estimate + STORAGE_SAFETY_BYTES
220
262
  if available < required:
@@ -229,6 +271,9 @@ def _prepare_source(message: Dict[str, Any]) -> Dict[str, Any]:
229
271
  shutil.rmtree(job_dir, ignore_errors=True)
230
272
  raise
231
273
  source_extra_required = payload.stat().st_size
274
+ if source_extra_required > MAX_TRANSFER_PAYLOAD_BYTES:
275
+ shutil.rmtree(job_dir, ignore_errors=True)
276
+ raise ValueError(f"Transfer payload exceeds the {MAX_TRANSFER_PAYLOAD_BYTES} byte limit")
232
277
  archive_format = "tar"
233
278
 
234
279
  payload_size = payload.stat().st_size
@@ -273,6 +318,10 @@ def _prepare_destination(message: Dict[str, Any]) -> Dict[str, Any]:
273
318
  entry_count = int(message.get("entry_count") or 1)
274
319
  if payload_size < 0 or expanded_storage < 0:
275
320
  raise ValueError("payload_size and expanded_storage_bytes must be non-negative")
321
+ if payload_size > MAX_TRANSFER_PAYLOAD_BYTES:
322
+ raise ValueError(f"Transfer payload exceeds the {MAX_TRANSFER_PAYLOAD_BYTES} byte limit")
323
+ if entry_count < 1 or entry_count > MAX_TRANSFER_ENTRIES:
324
+ raise ValueError(f"entry_count must be between 1 and {MAX_TRANSFER_ENTRIES}")
276
325
  if destination.exists() and not bool(message.get("overwrite", False)):
277
326
  raise ValueError(f"Destination already exists: {destination}")
278
327
  job_dir = _job_dir(transfer_id)
@@ -290,7 +339,8 @@ def _prepare_destination(message: Dict[str, Any]) -> Dict[str, Any]:
290
339
  shutil.rmtree(job_dir, ignore_errors=True)
291
340
  raise _storage_error(side="destination", path=destination, required=required,
292
341
  available=available, archive=payload_size, extraction=extraction)
293
- partial = job_dir / "incoming.part"
342
+ staging_parent = _nearest_existing_parent(destination)
343
+ partial = staging_parent / f".portacode-incoming-{transfer_id}.part"
294
344
  with partial.open("wb") as stream:
295
345
  stream.truncate(payload_size)
296
346
  manifest = {
@@ -302,6 +352,8 @@ def _prepare_destination(message: Dict[str, Any]) -> Dict[str, Any]:
302
352
  "state": "receiving",
303
353
  "destination_path": str(destination),
304
354
  "partial_path": str(partial),
355
+ "staged_path": str(staging_parent / f".portacode-staged-{transfer_id}"),
356
+ "extraction_path": str(staging_parent / f".portacode-extract-{transfer_id}"),
305
357
  "payload_size": payload_size,
306
358
  "payload_hash": str(message.get("payload_hash") or ""),
307
359
  "chunk_size": min(max(int(message.get("chunk_size") or DEFAULT_CHUNK_SIZE), MIN_CHUNK_SIZE), MAX_CHUNK_SIZE),
@@ -409,13 +461,11 @@ def _safe_members(archive: tarfile.TarFile, root: Path) -> Iterable[tarfile.TarI
409
461
  raise ValueError(f"Archive path escapes destination: {member.name}")
410
462
  if member.ischr() or member.isblk() or member.isfifo():
411
463
  raise ValueError(f"Unsupported special file in archive: {member.name}")
464
+ # Reject links entirely. Merely checking each link target is not
465
+ # sufficient because a later member can traverse a symlink created by
466
+ # an earlier member during extraction.
412
467
  if member.issym() or member.islnk():
413
- link = PurePosixPath(member.linkname)
414
- if link.is_absolute():
415
- raise ValueError(f"Absolute archive link is forbidden: {member.name}")
416
- link_destination = (destination.parent / Path(*link.parts)).resolve()
417
- if os.path.commonpath((str(root_resolved), str(link_destination))) != str(root_resolved):
418
- raise ValueError(f"Archive link escapes destination: {member.name}")
468
+ raise ValueError(f"Archive links are forbidden: {member.name}")
419
469
  yield member
420
470
 
421
471
 
@@ -427,7 +477,7 @@ def _apply_metadata(path: Path, metadata: Dict[str, Any]) -> Dict[str, bool]:
427
477
  except (OSError, NotImplementedError):
428
478
  # Some Unix builds expose follow_symlinks but do not implement it for
429
479
  # chmod. Staged file payloads are verified regular files.
430
- if path.is_file() and not path.is_symlink():
480
+ if (path.is_file() or path.is_dir()) and not path.is_symlink():
431
481
  try:
432
482
  os.chmod(path, int(metadata["mode"]))
433
483
  result["mode"] = True
@@ -448,6 +498,34 @@ def _apply_metadata(path: Path, metadata: Dict[str, Any]) -> Dict[str, bool]:
448
498
  return result
449
499
 
450
500
 
501
+ def _restore_archive_metadata(root: Path, members: Iterable[tarfile.TarInfo]) -> Dict[str, bool]:
502
+ """Restore validated tar metadata consistently across Python versions.
503
+
504
+ Python 3.14's default tar extraction filter intentionally removes group
505
+ and other write bits. The archive has already passed our stricter path,
506
+ link, and special-file validation, so reapply its recorded metadata after
507
+ extraction. Directories are handled last so creating their children does
508
+ not overwrite their timestamps.
509
+ """
510
+ results = {"mode": True, "ownership": True, "timestamps": True}
511
+ ordered = sorted(members, key=lambda item: (item.isdir(), -len(PurePosixPath(item.name).parts)))
512
+ for member in ordered:
513
+ target = root / Path(*PurePosixPath(member.name).parts)
514
+ pax = member.pax_headers or {}
515
+ timestamp = float(pax.get("mtime", member.mtime))
516
+ metadata = {
517
+ "mode": member.mode,
518
+ "uid": member.uid,
519
+ "gid": member.gid,
520
+ "atime_ns": int(float(pax.get("atime", timestamp)) * 1_000_000_000),
521
+ "mtime_ns": int(timestamp * 1_000_000_000),
522
+ }
523
+ applied = _apply_metadata(target, metadata)
524
+ for key, value in applied.items():
525
+ results[key] = results[key] and value
526
+ return results
527
+
528
+
451
529
  def _install_path(staged: Path, destination: Path, overwrite: bool) -> None:
452
530
  destination.parent.mkdir(parents=True, exist_ok=True)
453
531
  backup = None
@@ -485,12 +563,15 @@ def _finalize(message: Dict[str, Any]) -> Dict[str, Any]:
485
563
  destination = Path(manifest["destination_path"])
486
564
  metadata_result: Dict[str, bool] = {}
487
565
  if manifest["kind"] == "file":
488
- staged = partial.with_name("staged-file")
566
+ staged = Path(manifest.get("staged_path") or partial.with_name(f".portacode-staged-{manifest['transfer_id']}"))
567
+ staged.unlink(missing_ok=True)
489
568
  os.replace(partial, staged)
490
569
  metadata_result = _apply_metadata(staged, manifest.get("metadata") or {})
491
570
  _install_path(staged, destination, bool(manifest.get("overwrite")))
492
571
  else:
493
- extraction_root = _job_dir(manifest["transfer_id"]) / "extracted"
572
+ extraction_root = Path(manifest.get("extraction_path") or (
573
+ _nearest_existing_parent(destination) / f".portacode-extract-{manifest['transfer_id']}"
574
+ ))
494
575
  shutil.rmtree(extraction_root, ignore_errors=True)
495
576
  extraction_root.mkdir(mode=0o700)
496
577
  with tarfile.open(partial, "r:") as archive:
@@ -498,6 +579,7 @@ def _finalize(message: Dict[str, Any]) -> Dict[str, Any]:
498
579
  if len(members) != int(manifest.get("entry_count") or len(members)):
499
580
  raise ValueError("Archive entry count differs from the authorized manifest")
500
581
  archive.extractall(extraction_root, members=members, numeric_owner=True)
582
+ metadata_result = _restore_archive_metadata(extraction_root, members)
501
583
  roots = list(extraction_root.iterdir())
502
584
  if len(roots) != 1:
503
585
  raise ValueError("Folder archive must contain exactly one root item")
@@ -578,5 +660,6 @@ class TransferCancelHandler(SyncHandler):
578
660
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
579
661
  manifest = _load_manifest(message.get("transfer_id"), message.get("authorization_token"))
580
662
  transfer_id = manifest["transfer_id"]
663
+ _cleanup_destination_staging(manifest)
581
664
  shutil.rmtree(_job_dir(transfer_id), ignore_errors=True)
582
665
  return {"event": "transfer_canceled", "transfer_id": transfer_id, "success": True}
@@ -13,6 +13,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
13
13
  from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
14
14
 
15
15
  from portacode.codex_loopback_proxy import (
16
+ MAX_FORWARD_REQUEST_BYTES,
16
17
  MAX_REQUEST_BYTES,
17
18
  MAX_TOOL_OUTPUT_BYTES,
18
19
  CodexLoopbackProxy,
@@ -82,6 +83,33 @@ def test_ingress_limit_leaves_room_to_crop_oversized_tool_output():
82
83
  assert len(sanitized) < len(body)
83
84
  assert changed == 1
84
85
  assert omitted > 10 * 1024 * 1024 - MAX_TOOL_OUTPUT_BYTES
86
+
87
+
88
+ def test_sanitize_omits_oldest_inline_tool_images_to_fit_forward_limit():
89
+ image = "data:image/png;base64," + ("a" * (3 * 1024 * 1024))
90
+ body = json.dumps({
91
+ "input": [
92
+ {
93
+ "type": "custom_tool_call_output",
94
+ "call_id": f"call-{index}",
95
+ "output": [
96
+ {"type": "input_text", "text": f"screenshot {index}"},
97
+ {"type": "input_image", "image_url": image, "detail": "high"},
98
+ ],
99
+ }
100
+ for index in range(4)
101
+ ]
102
+ }).encode()
103
+
104
+ sanitized, changed, omitted = sanitize_responses_request(body)
105
+ payload = json.loads(sanitized)
106
+
107
+ assert len(sanitized) <= MAX_FORWARD_REQUEST_BYTES
108
+ assert changed == 1
109
+ assert omitted >= len(image)
110
+ assert payload["input"][0]["output"][1]["type"] == "input_text"
111
+ assert "omitted an older inline tool screenshot" in payload["input"][0]["output"][1]["text"]
112
+ assert payload["input"][-1]["output"][1]["image_url"] == image
85
113
  from portacode.keypair import KeyPair
86
114
 
87
115
 
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import stat
5
+ import io
6
+ import tarfile
7
+ import uuid
8
+ from pathlib import Path
9
+ from unittest.mock import patch
10
+
11
+ import pytest
12
+
13
+ from portacode.connection.handlers.resumable_transfer_handlers import (
14
+ STORAGE_SAFETY_BYTES,
15
+ TransferFinalizeHandler,
16
+ TransferCancelHandler,
17
+ TransferPrepareHandler,
18
+ TransferReadChunkHandler,
19
+ TransferReceiveChunkHandler,
20
+ TransferStatusHandler,
21
+ )
22
+
23
+
24
+ TOKEN = "transfer-authorization-token-with-ample-entropy"
25
+
26
+
27
+ def _handler(handler_class):
28
+ return handler_class(None, {})
29
+
30
+
31
+ def _copy_all_chunks(source_cache: Path, destination_cache: Path, prepared: dict, destination: Path):
32
+ transfer_id = prepared["transfer_id"]
33
+ common = {
34
+ "transfer_id": transfer_id,
35
+ "authorization_token": TOKEN,
36
+ }
37
+ os.environ["PORTACODE_TRANSFER_CACHE_DIR"] = str(destination_cache)
38
+ destination_prepared = _handler(TransferPrepareHandler).execute({
39
+ **common,
40
+ "role": "destination",
41
+ "kind": prepared["kind"],
42
+ "path": str(destination),
43
+ "payload_size": prepared["payload_size"],
44
+ "payload_hash": prepared["payload_hash"],
45
+ "chunk_size": prepared["chunk_size"],
46
+ "chunk_count": prepared["chunk_count"],
47
+ "expanded_size": prepared["expanded_size"],
48
+ "expanded_storage_bytes": prepared["expanded_storage_bytes"],
49
+ "entry_count": prepared["entry_count"],
50
+ "metadata": prepared["metadata"],
51
+ "archive_format": prepared["archive_format"],
52
+ })
53
+ assert destination_prepared["progress_percent"] == 0
54
+
55
+ for index in range(prepared["chunk_count"]):
56
+ os.environ["PORTACODE_TRANSFER_CACHE_DIR"] = str(source_cache)
57
+ chunk = _handler(TransferReadChunkHandler).execute({**common, "chunk_index": index})
58
+ os.environ["PORTACODE_TRANSFER_CACHE_DIR"] = str(destination_cache)
59
+ received = _handler(TransferReceiveChunkHandler).execute({
60
+ **common,
61
+ "chunk_index": index,
62
+ "chunk_hash": chunk["chunk_hash"],
63
+ "content_base64": chunk["content_base64"],
64
+ })
65
+ expected = round(received["completed_bytes"] * 100 / max(received["payload_size"], 1), 2)
66
+ assert received["progress_percent"] == expected
67
+
68
+ # A newly constructed handler reads the persisted receipt map, simulating
69
+ # both a WebSocket reconnect and a device-agent restart.
70
+ status = _handler(TransferStatusHandler).execute(common)
71
+ assert status["missing_chunks"] == []
72
+ assert status["progress_percent"] == 100
73
+ return _handler(TransferFinalizeHandler).execute(common)
74
+
75
+
76
+ def test_file_transfer_resumes_and_preserves_metadata(tmp_path, monkeypatch):
77
+ source_cache = tmp_path / "source-cache"
78
+ destination_cache = tmp_path / "destination-cache"
79
+ source = tmp_path / "source.bin"
80
+ source.write_bytes((b"portacode-transfer" * 20000) + b"tail")
81
+ source.chmod(0o640)
82
+ transfer_id = str(uuid.uuid4())
83
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(source_cache))
84
+ prepared = _handler(TransferPrepareHandler).execute({
85
+ "role": "source",
86
+ "kind": "file",
87
+ "path": str(source),
88
+ "transfer_id": transfer_id,
89
+ "authorization_token": TOKEN,
90
+ "chunk_size": 64 * 1024,
91
+ })
92
+
93
+ destination = tmp_path / "received.bin"
94
+ finalized = _copy_all_chunks(source_cache, destination_cache, prepared, destination)
95
+
96
+ assert destination.read_bytes() == source.read_bytes()
97
+ assert stat.S_IMODE(destination.stat().st_mode) == 0o640
98
+ assert finalized["complete"] is True
99
+ assert finalized["metadata_preserved"]["mode"] is True
100
+
101
+
102
+ def test_folder_transfer_resumes_and_preserves_tree_metadata(tmp_path, monkeypatch):
103
+ source_cache = tmp_path / "source-cache"
104
+ destination_cache = tmp_path / "destination-cache"
105
+ source = tmp_path / "assets"
106
+ (source / "nested").mkdir(parents=True)
107
+ (source / "nested" / "photo.bin").write_bytes(b"image" * 30000)
108
+ (source / "readme.txt").write_text("hello", encoding="utf-8")
109
+ (source / "nested" / "photo.bin").chmod(0o600)
110
+ (source / "readme.txt").chmod(0o664)
111
+ (source / "nested").chmod(0o770)
112
+ transfer_id = str(uuid.uuid4())
113
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(source_cache))
114
+ prepared = _handler(TransferPrepareHandler).execute({
115
+ "role": "source",
116
+ "kind": "folder",
117
+ "path": str(source),
118
+ "transfer_id": transfer_id,
119
+ "authorization_token": TOKEN,
120
+ "chunk_size": 64 * 1024,
121
+ })
122
+
123
+ destination = tmp_path / "copied-assets"
124
+ finalized = _copy_all_chunks(source_cache, destination_cache, prepared, destination)
125
+
126
+ assert (destination / "readme.txt").read_text(encoding="utf-8") == "hello"
127
+ assert (destination / "nested" / "photo.bin").read_bytes() == b"image" * 30000
128
+ assert stat.S_IMODE((destination / "nested" / "photo.bin").stat().st_mode) == 0o600
129
+ assert stat.S_IMODE((destination / "readme.txt").stat().st_mode) == 0o664
130
+ assert stat.S_IMODE((destination / "nested").stat().st_mode) == 0o770
131
+ assert finalized["kind"] == "folder"
132
+ assert finalized["metadata_preserved"]["mode"] is True
133
+
134
+
135
+ def test_transfer_token_is_bound_and_required(tmp_path, monkeypatch):
136
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(tmp_path / "cache"))
137
+ source = tmp_path / "source.txt"
138
+ source.write_text("secret", encoding="utf-8")
139
+ transfer_id = str(uuid.uuid4())
140
+ _handler(TransferPrepareHandler).execute({
141
+ "role": "source",
142
+ "kind": "file",
143
+ "path": str(source),
144
+ "transfer_id": transfer_id,
145
+ "authorization_token": TOKEN,
146
+ })
147
+
148
+ with pytest.raises(PermissionError, match="authorization is invalid"):
149
+ _handler(TransferStatusHandler).execute({
150
+ "transfer_id": transfer_id,
151
+ "authorization_token": "a-different-token-with-enough-characters",
152
+ })
153
+
154
+
155
+ def test_destination_reports_peak_storage_deficit(tmp_path, monkeypatch):
156
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(tmp_path / "cache"))
157
+ transfer_id = str(uuid.uuid4())
158
+ payload_size = 20 * 1024 * 1024
159
+ expanded = 80 * 1024 * 1024
160
+ available = 10 * 1024 * 1024
161
+ with patch(
162
+ "portacode.connection.handlers.resumable_transfer_handlers._free_bytes",
163
+ return_value=available,
164
+ ), patch(
165
+ "portacode.connection.handlers.resumable_transfer_handlers._filesystem_block_size",
166
+ return_value=4096,
167
+ ), pytest.raises(ValueError) as error:
168
+ _handler(TransferPrepareHandler).execute({
169
+ "role": "destination",
170
+ "kind": "folder",
171
+ "path": str(tmp_path / "destination"),
172
+ "transfer_id": transfer_id,
173
+ "authorization_token": TOKEN,
174
+ "payload_size": payload_size,
175
+ "payload_hash": "a" * 64,
176
+ "expanded_storage_bytes": expanded,
177
+ "chunk_size": 64 * 1024,
178
+ "chunk_count": payload_size // (64 * 1024),
179
+ })
180
+ entry_overhead = 4096
181
+ required = payload_size + expanded + entry_overhead + STORAGE_SAFETY_BYTES
182
+ message = str(error.value)
183
+ assert f"required {required} bytes" in message
184
+ assert f"available {available} bytes" in message
185
+ assert f"short by {required - available} bytes" in message
186
+
187
+
188
+ def test_folder_finalize_rejects_symlink_chain_archive(tmp_path, monkeypatch):
189
+ cache = tmp_path / "cache"
190
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(cache))
191
+ transfer_id = str(uuid.uuid4())
192
+ payload = tmp_path / "hostile.tar"
193
+ with tarfile.open(payload, "w") as archive:
194
+ root = tarfile.TarInfo("root/")
195
+ root.type = tarfile.DIRTYPE
196
+ archive.addfile(root)
197
+ link = tarfile.TarInfo("root/pivot")
198
+ link.type = tarfile.SYMTYPE
199
+ link.linkname = "."
200
+ archive.addfile(link)
201
+ content = b"must not extract"
202
+ nested = tarfile.TarInfo("root/pivot/file.txt")
203
+ nested.size = len(content)
204
+ archive.addfile(nested, io.BytesIO(content))
205
+
206
+ digest = __import__("hashlib").sha256(payload.read_bytes()).hexdigest()
207
+ common = {"transfer_id": transfer_id, "authorization_token": TOKEN}
208
+ prepared = _handler(TransferPrepareHandler).execute({
209
+ **common, "role": "destination", "kind": "folder",
210
+ "path": str(tmp_path / "output"), "payload_size": payload.stat().st_size,
211
+ "payload_hash": digest, "chunk_size": 64 * 1024,
212
+ "chunk_count": 1, "expanded_storage_bytes": len(content), "entry_count": 3,
213
+ })
214
+ received = _handler(TransferReceiveChunkHandler).execute({
215
+ **common, "chunk_index": 0, "chunk_hash": digest,
216
+ "content_base64": __import__("base64").b64encode(payload.read_bytes()).decode(),
217
+ })
218
+ assert received["progress_percent"] == 100
219
+ with pytest.raises(ValueError, match="Archive links are forbidden"):
220
+ _handler(TransferFinalizeHandler).execute(common)
221
+ assert not (tmp_path / "output").exists()
222
+
223
+
224
+ def test_destination_stages_on_destination_filesystem_and_cancel_cleans_it(tmp_path, monkeypatch):
225
+ cache = tmp_path / "cache"
226
+ destination_root = tmp_path / "destination-filesystem"
227
+ destination_root.mkdir()
228
+ destination = destination_root / "nested" / "received.bin"
229
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(cache))
230
+ transfer_id = str(uuid.uuid4())
231
+ common = {"transfer_id": transfer_id, "authorization_token": TOKEN}
232
+ _handler(TransferPrepareHandler).execute({
233
+ **common, "role": "destination", "kind": "file", "path": str(destination),
234
+ "payload_size": 4, "payload_hash": "0" * 64, "chunk_size": 32768,
235
+ "chunk_count": 1, "expanded_storage_bytes": 4, "entry_count": 1,
236
+ })
237
+ manifest = __import__("json").loads((cache / transfer_id / "manifest.json").read_text())
238
+ partial = Path(manifest["partial_path"])
239
+ assert partial.parent == destination_root
240
+ assert partial.exists()
241
+ _handler(TransferCancelHandler).execute(common)
242
+ assert not partial.exists()
243
+ assert not (cache / transfer_id).exists()
244
+
245
+
246
+ def test_folder_source_rejects_links_before_archiving(tmp_path, monkeypatch):
247
+ source = tmp_path / "source"
248
+ source.mkdir()
249
+ (source / "real.txt").write_text("data", encoding="utf-8")
250
+ (source / "link.txt").symlink_to("real.txt")
251
+ monkeypatch.setenv("PORTACODE_TRANSFER_CACHE_DIR", str(tmp_path / "cache"))
252
+ with pytest.raises(ValueError, match="do not support symbolic links"):
253
+ _handler(TransferPrepareHandler).execute({
254
+ "role": "source", "kind": "folder", "path": str(source),
255
+ "transfer_id": str(uuid.uuid4()), "authorization_token": TOKEN,
256
+ })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.5.21.dev0
3
+ Version: 1.5.21.dev2
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian