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.
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/PKG-INFO +1 -1
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/_version.py +2 -2
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/codex_loopback_proxy.py +48 -6
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +10 -5
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/resumable_transfer_handlers.py +100 -17
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_loopback_proxy.py +28 -0
- portacode-1.5.21.dev2/portacode/tests/test_resumable_transfer_handlers.py +256 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/SOURCES.txt +1 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_play_store_screenshots.py +149 -18
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/cli.py +15 -3
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/hierarchical_runner.py +12 -9
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/runner.py +16 -8
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/run_screenshot_suite.sh +51 -11
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.claude/settings.local.json +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.gitignore +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/.gitmodules +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/LICENSE +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/MANIFEST.in +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/Makefile +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/build_android.sh +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/connect.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/connect.sh +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/creative-team-brief-portacode.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/device-health-alerting-plan.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/devops-messaging-ab-tests.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/homepage-dashboard-positioning-fixes.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/device-transfer-button.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/device-transfer-modal.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/pair-device-button.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/pairing-request.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/images/student-workspace.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/docs/template-guide.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {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
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/__main__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/cli.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/codex_prepare.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/codex_usage_limit.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/client.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/codex_app_server.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/codex_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/proxmox_infra.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/runtime_user.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_file_move_copy.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/test_runtime_user.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/multiplex.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/terminal.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/test_webmin_proxy_config.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/webmin_proxy_config.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/data.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/exit_codes.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/github_credential.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/keypair.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/links +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/logging_categories.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/pairing.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/restart.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/service.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/singleton.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_runtime_paths.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_service.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_singleton.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/test_terminal_exposed_services.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_app_server.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_handlers.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_prepare.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_github_credential.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/cloudflared_login.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/ensure_cloudflared.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/ensure_pyyaml.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/forwarding_state.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/get_domain.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/privileged.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/service_install.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tunneling/state.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/updater.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/utils/runtime_paths.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/.gitignore +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/Dockerfile +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/entrypoint.sh +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/persistent_workspace/manage_classroom.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/Dockerfile +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/docker-compose.yaml +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode_for_school/temporary_workspace/entrypoint.sh +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/pyproject.toml +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/run_tests.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/scripts/diagnose_codex_gateway.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/setup.cfg +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/setup.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test.sh +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_device_online.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_file_operations.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_login_flow.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_request_id.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/test_session_terminal_shutdown.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/.env.example +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/README.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/__init__.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/base_test.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/testing_framework/requirements.txt +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/agent_context_management.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/django_server_time_sync.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/websocket_client_silently_dead.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/pairing_tester.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/validate.sh +0 -0
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 5, 21, '
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
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(
|
{portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -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.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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 =
|
|
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}
|
{portacode-1.5.21.dev0 → portacode-1.5.21.dev2}/portacode/tests/test_codex_loopback_proxy.py
RENAMED
|
@@ -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
|
+
})
|