remo-cli 2.2.0rc2__tar.gz → 2.2.0rc3__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 (413) hide show
  1. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/PKG-INFO +1 -1
  2. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docker/Dockerfile +26 -7
  3. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docker/compose.example.yml +27 -11
  4. remo_cli-2.2.0rc3/docker/entrypoint.sh +88 -0
  5. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/web-session-interface.md +1 -1
  6. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/pyproject.toml +1 -1
  7. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/image/test_docker_image.py +254 -4
  8. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/uv.lock +1 -1
  9. remo_cli-2.2.0rc2/docker/entrypoint.sh +0 -37
  10. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-analyze/SKILL.md +0 -0
  11. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-checklist/SKILL.md +0 -0
  12. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-clarify/SKILL.md +0 -0
  13. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-constitution/SKILL.md +0 -0
  14. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-converge/SKILL.md +0 -0
  15. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
  16. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
  17. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
  18. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
  19. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
  20. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-implement/SKILL.md +0 -0
  21. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-plan/SKILL.md +0 -0
  22. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-specify/SKILL.md +0 -0
  23. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-tasks/SKILL.md +0 -0
  24. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
  25. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.devcontainer/devcontainer.json +0 -0
  26. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.dockerignore +0 -0
  27. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/dependabot.yml +0 -0
  28. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/workflows/ci.yml +0 -0
  29. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/workflows/provision.yml +0 -0
  30. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/workflows/release.yml +0 -0
  31. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/workflows/smoke-test-cleanup.yml +0 -0
  32. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/workflows/smoke-test.yml +0 -0
  33. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.github/workflows/teardown.yml +0 -0
  34. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.gitignore +0 -0
  35. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
  36. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
  37. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.maverick/checkpoints/feature/validation_complete.json +0 -0
  38. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/.registry +0 -0
  39. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/README.md +0 -0
  40. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
  41. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
  42. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
  43. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
  44. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
  45. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/config-template.yml +0 -0
  46. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/extension.yml +0 -0
  47. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/git-config.yml +0 -0
  48. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
  49. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
  50. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
  51. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
  52. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
  53. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
  54. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
  55. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
  56. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/extensions.yml +0 -0
  57. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/feature.json +0 -0
  58. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/init-options.json +0 -0
  59. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/integration.json +0 -0
  60. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/integrations/claude.manifest.json +0 -0
  61. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/integrations/speckit.manifest.json +0 -0
  62. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/memory/constitution.md +0 -0
  63. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  64. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/scripts/bash/common.sh +0 -0
  65. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/scripts/bash/create-new-feature.sh +0 -0
  66. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/scripts/bash/setup-plan.sh +0 -0
  67. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/scripts/bash/setup-tasks.sh +0 -0
  68. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/scripts/bash/update-agent-context.sh +0 -0
  69. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/templates/agent-file-template.md +0 -0
  70. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/templates/checklist-template.md +0 -0
  71. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/templates/constitution-template.md +0 -0
  72. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/templates/plan-template.md +0 -0
  73. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/templates/spec-template.md +0 -0
  74. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/templates/tasks-template.md +0 -0
  75. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/workflows/speckit/workflow.yml +0 -0
  76. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/.specify/workflows/workflow-registry.json +0 -0
  77. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/AGENTS.md +0 -0
  78. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/CLAUDE.md +0 -0
  79. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/CONTRIBUTING.md +0 -0
  80. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/Dockerfile +0 -0
  81. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/LICENSE +0 -0
  82. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/README.md +0 -0
  83. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/.ansible-lint +0 -0
  84. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/README.md +0 -0
  85. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/ansible.cfg +0 -0
  86. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/aws_configure.yml +0 -0
  87. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/aws_resize.yml +0 -0
  88. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/aws_site.yml +0 -0
  89. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/aws_teardown.yml +0 -0
  90. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/group_vars/all.yml +0 -0
  91. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/group_vars/incus_containers.yml +0 -0
  92. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/group_vars/proxmox_containers.yml +0 -0
  93. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/hetzner_configure.yml +0 -0
  94. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/hetzner_provision.yml +0 -0
  95. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/hetzner_resize.yml +0 -0
  96. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/hetzner_site.yml +0 -0
  97. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/hetzner_teardown.yml +0 -0
  98. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/incus_bootstrap.yml +0 -0
  99. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/incus_configure.yml +0 -0
  100. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/incus_provision.yml +0 -0
  101. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/incus_resize.yml +0 -0
  102. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/incus_site.yml +0 -0
  103. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/incus_teardown.yml +0 -0
  104. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/inventory/hosts.yml +0 -0
  105. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/inventory/incus_containers.yml +0 -0
  106. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/proxmox_bootstrap.yml +0 -0
  107. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/proxmox_configure.yml +0 -0
  108. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/proxmox_provision.yml +0 -0
  109. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/proxmox_resize.yml +0 -0
  110. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/proxmox_site.yml +0 -0
  111. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/proxmox_teardown.yml +0 -0
  112. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/requirements.yml +0 -0
  113. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/defaults/main.yml +0 -0
  114. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
  115. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
  116. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/tasks/main.yml +0 -0
  117. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
  118. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
  119. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
  120. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/deacon/defaults/main.yml +0 -0
  121. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/deacon/tasks/main.yml +0 -0
  122. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/devcontainers/tasks/main.yml +0 -0
  123. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/docker/handlers/main.yml +0 -0
  124. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/docker/tasks/main.yml +0 -0
  125. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/fzf/tasks/main.yml +0 -0
  126. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/github_cli/tasks/main.yml +0 -0
  127. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
  128. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
  129. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
  130. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
  131. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
  132. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container/defaults/main.yml +0 -0
  133. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container/handlers/main.yml +0 -0
  134. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container/meta/main.yml +0 -0
  135. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container/tasks/main.yml +0 -0
  136. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
  137. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
  138. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
  139. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
  140. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/nodejs/defaults/main.yml +0 -0
  141. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/nodejs/tasks/main.yml +0 -0
  142. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
  143. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
  144. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
  145. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
  146. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
  147. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container/meta/main.yml +0 -0
  148. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
  149. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
  150. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
  151. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
  152. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
  153. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/user_setup/defaults/main.yml +0 -0
  154. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/user_setup/tasks/main.yml +0 -0
  155. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
  156. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/user_setup/templates/project-launch.sh.j2 +0 -0
  157. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
  158. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/user_setup/templates/remo-host.sh.j2 +0 -0
  159. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/zellij/defaults/main.yml +0 -0
  160. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/zellij/tasks/main.yml +0 -0
  161. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
  162. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
  163. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/ansible/tasks/configure_dev_tools.yml +0 -0
  164. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/aws.md +0 -0
  165. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/hetzner.md +0 -0
  166. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/incus.md +0 -0
  167. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/install.sh +0 -0
  168. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/proxmox.md +0 -0
  169. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/remo-web-speckit-input.md +0 -0
  170. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/docs/remo-web.html +0 -0
  171. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/index.html +0 -0
  172. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/package-lock.json +0 -0
  173. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/package.json +0 -0
  174. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/public/README.md +0 -0
  175. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/public/dist__ghostty-vt.wasm +0 -0
  176. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/public/ghostty-vt.wasm +0 -0
  177. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/scripts/copy-ghostty-wasm.mjs +0 -0
  178. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/api/client.ts +0 -0
  179. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/AppRoot.tsx +0 -0
  180. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/AppShell.css +0 -0
  181. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/AppShell.tsx +0 -0
  182. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/AwaitingAdoption.css +0 -0
  183. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/AwaitingAdoption.tsx +0 -0
  184. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/OfflineOverlay.css +0 -0
  185. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/OfflineOverlay.tsx +0 -0
  186. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/SessionRail.css +0 -0
  187. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/SessionRail.tsx +0 -0
  188. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/SettingsPage.css +0 -0
  189. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/SettingsPage.tsx +0 -0
  190. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/ShortcutsModal.css +0 -0
  191. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/ShortcutsModal.tsx +0 -0
  192. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/TerminalCard.css +0 -0
  193. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/TerminalCard.tsx +0 -0
  194. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/TopBar.css +0 -0
  195. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/TopBar.tsx +0 -0
  196. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/WorkspacePane.css +0 -0
  197. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/WorkspacePane.tsx +0 -0
  198. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/providerMeta.ts +0 -0
  199. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/components/railModel.ts +0 -0
  200. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/main.tsx +0 -0
  201. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/state/discovery.ts +0 -0
  202. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/state/fonts.ts +0 -0
  203. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/state/health.ts +0 -0
  204. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/state/settings.ts +0 -0
  205. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/state/useConsoleKeyboard.ts +0 -0
  206. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/state/workspace.ts +0 -0
  207. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/terminal/.gitkeep +0 -0
  208. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/terminal/GhosttyRenderer.ts +0 -0
  209. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/terminal/RendererAdapter.ts +0 -0
  210. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/terminal/TerminalConnection.ts +0 -0
  211. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/terminal/XtermRenderer.ts +0 -0
  212. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/terminal/defaultRenderer.ts +0 -0
  213. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/theme/fonts.ts +0 -0
  214. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/src/theme/tokens.css +0 -0
  215. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/tsconfig.json +0 -0
  216. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/frontend/vite.config.ts +0 -0
  217. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/install.sh +0 -0
  218. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/maverick.yaml +0 -0
  219. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/playwright.config.ts +0 -0
  220. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/project-menu.sh +0 -0
  221. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/remo +0 -0
  222. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/run.sh +0 -0
  223. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/scripts/setup-aws-oidc.sh +0 -0
  224. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
  225. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
  226. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
  227. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
  228. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/data-model.md +0 -0
  229. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/plan.md +0 -0
  230. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
  231. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/research.md +0 -0
  232. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/spec.md +0 -0
  233. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/001-bootstrap-incus-host/tasks.md +0 -0
  234. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/checklists/requirements.md +0 -0
  235. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
  236. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
  237. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/data-model.md +0 -0
  238. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/plan.md +0 -0
  239. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/quickstart.md +0 -0
  240. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/research.md +0 -0
  241. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/spec.md +0 -0
  242. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/002-incus-container-support/tasks.md +0 -0
  243. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
  244. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/data-model.md +0 -0
  245. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/plan.md +0 -0
  246. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/quickstart.md +0 -0
  247. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/research.md +0 -0
  248. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/spec.md +0 -0
  249. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/003-python-cli-rewrite/tasks.md +0 -0
  250. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/004-proxmox-container-support/spec.md +0 -0
  251. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/checklists/requirements.md +0 -0
  252. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/contracts/cli-surface.md +0 -0
  253. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/data-model.md +0 -0
  254. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/plan.md +0 -0
  255. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/quickstart.md +0 -0
  256. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/research.md +0 -0
  257. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/spec.md +0 -0
  258. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/005-provider-snapshots/tasks.md +0 -0
  259. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/checklists/requirements.md +0 -0
  260. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/contracts/remo-host-protocol.md +0 -0
  261. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/contracts/rest-api.md +0 -0
  262. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/contracts/terminal-websocket.md +0 -0
  263. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/data-model.md +0 -0
  264. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/plan.md +0 -0
  265. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/quickstart.md +0 -0
  266. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/research.md +0 -0
  267. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/spec.md +0 -0
  268. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/010-web-session-interface/tasks.md +0 -0
  269. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/checklists/requirements.md +0 -0
  270. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/contracts/cli-web-adopt.md +0 -0
  271. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/contracts/setup-api.md +0 -0
  272. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/data-model.md +0 -0
  273. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/plan.md +0 -0
  274. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/quickstart-results.md +0 -0
  275. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/quickstart.md +0 -0
  276. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/research.md +0 -0
  277. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/spec.md +0 -0
  278. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/specs/011-web-adopt/tasks.md +0 -0
  279. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/__init__.py +0 -0
  280. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/__main__.py +0 -0
  281. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/__init__.py +0 -0
  282. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/cp.py +0 -0
  283. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/main.py +0 -0
  284. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/providers/__init__.py +0 -0
  285. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/providers/aws.py +0 -0
  286. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/providers/hetzner.py +0 -0
  287. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/providers/incus.py +0 -0
  288. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/providers/proxmox.py +0 -0
  289. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/shell.py +0 -0
  290. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/cli/web.py +0 -0
  291. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/__init__.py +0 -0
  292. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/ansible_runner.py +0 -0
  293. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/completion.py +0 -0
  294. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/config.py +0 -0
  295. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/known_hosts.py +0 -0
  296. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/output.py +0 -0
  297. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/picker.py +0 -0
  298. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/remo_host_client.py +0 -0
  299. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/rsync.py +0 -0
  300. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/snapshot.py +0 -0
  301. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/ssh.py +0 -0
  302. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/validation.py +0 -0
  303. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/version.py +0 -0
  304. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/core/web_adopt.py +0 -0
  305. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/models/__init__.py +0 -0
  306. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/models/capability.py +0 -0
  307. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/models/discovery.py +0 -0
  308. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/models/host.py +0 -0
  309. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/models/session_target.py +0 -0
  310. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/models/snapshot.py +0 -0
  311. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/providers/__init__.py +0 -0
  312. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/providers/aws.py +0 -0
  313. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/providers/hetzner.py +0 -0
  314. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/providers/incus.py +0 -0
  315. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/providers/proxmox.py +0 -0
  316. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/__init__.py +0 -0
  317. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/api/__init__.py +0 -0
  318. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/api/hosts.py +0 -0
  319. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/api/setup.py +0 -0
  320. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/api/terminals.py +0 -0
  321. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/app.py +0 -0
  322. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/check.py +0 -0
  323. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/config.py +0 -0
  324. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/discovery.py +0 -0
  325. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/health.py +0 -0
  326. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/logging_config.py +0 -0
  327. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/models.py +0 -0
  328. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/ssh_master.py +0 -0
  329. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/state.py +0 -0
  330. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/terminal.py +0 -0
  331. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/terminal_registry.py +0 -0
  332. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/src/remo_cli/web/tokens.py +0 -0
  333. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/__init__.py +0 -0
  334. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/ansible/__init__.py +0 -0
  335. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/ansible/test_remo_host_idempotency.py +0 -0
  336. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/conftest.py +0 -0
  337. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/fixtures.ts +0 -0
  338. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/ghostty-shell-io.spec.ts +0 -0
  339. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/ghostty-tui-compatibility.spec.ts +0 -0
  340. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/keyboard-routing.spec.ts +0 -0
  341. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/mobile.spec.ts +0 -0
  342. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/reconnect.spec.ts +0 -0
  343. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/e2e/workspace-layout.spec.ts +0 -0
  344. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/image/__init__.py +0 -0
  345. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/__init__.py +0 -0
  346. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/_nine_terminal_fixture.py +0 -0
  347. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/orbstack-cloud-init.yml +0 -0
  348. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/orbstack.sh +0 -0
  349. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_nine_terminals.py +0 -0
  350. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_nine_terminals_soak.py +0 -0
  351. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_remo_host_e2e.py +0 -0
  352. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_security_rejections.py +0 -0
  353. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_terminal_attach.py +0 -0
  354. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_web_adopt_e2e.py +0 -0
  355. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/integration/test_web_cli_parity.py +0 -0
  356. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/perf/__init__.py +0 -0
  357. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/perf/test_latency.py +0 -0
  358. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/__init__.py +0 -0
  359. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/__init__.py +0 -0
  360. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/providers/test_aws_snapshot.py +0 -0
  361. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/providers/test_hetzner_snapshot.py +0 -0
  362. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/providers/test_incus_snapshot.py +0 -0
  363. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/providers/test_proxmox_snapshot.py +0 -0
  364. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/test_cp.py +0 -0
  365. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/test_main.py +0 -0
  366. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/test_shell.py +0 -0
  367. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/cli/test_web_adopt_cmd.py +0 -0
  368. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/__init__.py +0 -0
  369. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_ansible_runner.py +0 -0
  370. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_config.py +0 -0
  371. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_known_hosts.py +0 -0
  372. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_output.py +0 -0
  373. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_picker.py +0 -0
  374. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_remo_host_client.py +0 -0
  375. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_rsync.py +0 -0
  376. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_snapshot.py +0 -0
  377. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_ssh.py +0 -0
  378. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_ssh_controlpath.py +0 -0
  379. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_ssh_identity_opts.py +0 -0
  380. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_validation.py +0 -0
  381. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_version.py +0 -0
  382. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_web_adopt_authorize.py +0 -0
  383. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_web_adopt_payload.py +0 -0
  384. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_web_adopt_trust.py +0 -0
  385. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/core/test_web_push.py +0 -0
  386. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/models/__init__.py +0 -0
  387. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/models/test_capability.py +0 -0
  388. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/models/test_discovery.py +0 -0
  389. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/models/test_session_target.py +0 -0
  390. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/providers/__init__.py +0 -0
  391. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/providers/test_aws_snapshot.py +0 -0
  392. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/providers/test_hetzner_snapshot.py +0 -0
  393. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/providers/test_incus_snapshot.py +0 -0
  394. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/providers/test_proxmox_devcontainer_runtime.py +0 -0
  395. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/providers/test_proxmox_snapshot.py +0 -0
  396. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/test_ansible_templates.py +0 -0
  397. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/test_host_model.py +0 -0
  398. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/__init__.py +0 -0
  399. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/conftest.py +0 -0
  400. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_backpressure.py +0 -0
  401. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_check.py +0 -0
  402. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_csp.py +0 -0
  403. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_discovery.py +0 -0
  404. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_health_states.py +0 -0
  405. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_hosts_mapping.py +0 -0
  406. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_lazy_import.py +0 -0
  407. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_log_redaction.py +0 -0
  408. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_setup_api.py +0 -0
  409. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_setup_auth.py +0 -0
  410. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_state.py +0 -0
  411. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_terminal_resize.py +0 -0
  412. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_terminals_api.py +0 -0
  413. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc3}/tests/unit/web/test_tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remo-cli
3
- Version: 2.2.0rc2
3
+ Version: 2.2.0rc3
4
4
  Summary: Remote development environment CLI
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -60,8 +60,14 @@ WORKDIR /app
60
60
  # curl - AWS CLI/SSM plugin install below, and the container
61
61
  # healthcheck (compose.example.yml) at runtime
62
62
  # unzip - AWS CLI v2 install below
63
+ # gosu - drops from root to the unprivileged `remo` user in the
64
+ # entrypoint AFTER self-healing filesystem permissions
65
+ # (see docker/entrypoint.sh). Unlike `su`, it stays PID 1
66
+ # with correct signal semantics and adds no setuid-binary
67
+ # surface. Packaged for both amd64 and arm64 in Debian, so
68
+ # the multi-arch build is unaffected.
63
69
  RUN apt-get update && \
64
- apt-get install -y --no-install-recommends openssh-client curl unzip && \
70
+ apt-get install -y --no-install-recommends openssh-client curl unzip gosu && \
65
71
  rm -rf /var/lib/apt/lists/*
66
72
 
67
73
  # Install AWS CLI v2 (needed for SSM proxy), arch-selected via $TARGETARCH.
@@ -85,10 +91,10 @@ RUN if [ "$TARGETARCH" = "arm64" ]; then SM_ARCH="arm64"; else SM_ARCH="64bit";
85
91
  # Non-root user (UID/GID 1000 to match compose.example.yml `user: "1000:1000"`).
86
92
  RUN useradd -u 1000 -m -s /bin/bash remo
87
93
 
88
- # Install remo-cli[web] system-wide (as root, before USER remo) from the
89
- # wheel built in stage 2 — avoids needing build tools in this final layer
90
- # and avoids `pip install --user` (so no PATH fix-up is needed; `remo` /
91
- # `remo web` land directly on the system PATH for the `remo` user).
94
+ # Install remo-cli[web] system-wide (as root) from the wheel built in stage 2
95
+ # — avoids needing build tools in this final layer and avoids `pip install
96
+ # --user` (so no PATH fix-up is needed; `remo` / `remo web` land directly on
97
+ # the system PATH for the `remo` user the entrypoint drops to).
92
98
  COPY --from=wheel-builder /build/wheels/*.whl /tmp/
93
99
  RUN pip install --no-cache-dir "$(ls /tmp/*.whl)[web]" && \
94
100
  rm /tmp/*.whl
@@ -118,6 +124,19 @@ ENV REMO_WEB_FRONTEND_DIST_DIR=/app/frontend-dist
118
124
  # default, FR-052), same as any other containerized service.
119
125
  ENV REMO_WEB_BIND_HOST=0.0.0.0
120
126
 
121
- USER remo
122
-
127
+ # Pin HOME to the `remo` user's home so config-path resolution
128
+ # (core/config.py `Path.home()` / REMO_HOME) is identical whether the
129
+ # entrypoint is running its root-side healing pass or the dropped-to `remo`
130
+ # process — otherwise root's ambient HOME (/root) would resolve the config
131
+ # dir to a different path than the app later reads.
132
+ ENV HOME=/home/remo
133
+
134
+ # NOTE: intentionally NO `USER remo` here. The image starts as root so the
135
+ # entrypoint can self-heal ownership on a bind-mounted, root-owned config dir
136
+ # and on the /run/remo-ssh tmpfs (which a container *restart* remounts
137
+ # root-owned) BEFORE dropping — via gosu — to the unprivileged `remo` user.
138
+ # The app still ends up running as non-root `remo`; the drop just happens at
139
+ # runtime in the entrypoint instead of at image-build time. A deployer that
140
+ # sets an explicit `--user`/`user:` starts non-root, and the entrypoint skips
141
+ # healing (best-effort) and execs directly. See docker/entrypoint.sh.
123
142
  ENTRYPOINT ["/app/docker/entrypoint.sh"]
@@ -182,23 +182,39 @@ services:
182
182
  # a NEW identity on next boot; it revokes nothing on the instances —
183
183
  # remove the old `remo-web@<deployment-id>` authorized_keys line there.
184
184
  - "remo-web-state:/home/remo/.config/remo"
185
- # Identical hardening to the bind-mount service: adopted mode only moves
186
- # WHERE configuration comes from, never loosens the runtime posture.
187
- #
188
- # The tmpfs mode is pinned because an option-less tmpfs mount is
189
- # remounted root-owned 0755 (unwritable to uid 1000) by a container
190
- # *restart* — `docker compose restart`, or the restart policy reviving
191
- # the process — which would fail the startup runtime-dir check on every
192
- # boot after the first. `rw,mode=1777` is exactly what Docker mounts on
193
- # first boot anyway; pinning it just makes restarts behave the same.
185
+ # A PLAIN, option-less tmpfs no `rw,mode=1777` pin needed anymore. The
186
+ # image starts as root and its entrypoint re-heals ownership on
187
+ # /run/remo-ssh on EVERY start, so the well-known Docker quirk (an
188
+ # option-less tmpfs is remounted root-owned 0755 by a container *restart*)
189
+ # no longer breaks the startup runtime-dir check on the second boot.
194
190
  tmpfs:
195
- - "/run/remo-ssh:rw,mode=1777"
191
+ - "/run/remo-ssh"
196
192
  read_only: true
197
- user: "1000:1000"
193
+ # NOTE: no `user:` pin here (unlike the bind-mount service above). This
194
+ # service starts as ROOT so the entrypoint can self-heal ownership on the
195
+ # state volume (Docker seeds a fresh named volume root-owned; a bind-mount
196
+ # host dir is root-owned too) before dropping — via gosu — to the
197
+ # unprivileged `remo` user. The app STILL runs as non-root `remo`; only
198
+ # the brief healing pass runs as root. See docker/entrypoint.sh.
198
199
  security_opt:
199
200
  - "no-new-privileges:true"
201
+ # Drop everything, then add back ONLY the capabilities the heal-then-drop
202
+ # entrypoint needs while it is root: CHOWN (fix ownership of the state
203
+ # dir/tmpfs), DAC_OVERRIDE (traverse/write a dir owned by another uid),
204
+ # FOWNER (defensive — chmod/utime on files owned by another uid),
205
+ # SETUID+SETGID (gosu drops to the `remo` uid).
206
+ # The dropped-to `remo` process holds NONE of these effectively — dropping
207
+ # uid clears the permitted/effective sets — so the security posture is
208
+ # unchanged from a plain non-root run; `no-new-privileges` does not block
209
+ # gosu (dropping privileges is always allowed).
200
210
  cap_drop:
201
211
  - "ALL"
212
+ cap_add:
213
+ - "CHOWN"
214
+ - "DAC_OVERRIDE"
215
+ - "FOWNER"
216
+ - "SETUID"
217
+ - "SETGID"
202
218
  healthcheck:
203
219
  # /api/v1/ready answers 200 for BOTH `ready` and `unconfigured` — a
204
220
  # container awaiting adoption is healthy and must not crash-loop or
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env bash
2
+ # docker/entrypoint.sh
3
+ #
4
+ # Container entrypoint for `remo web` (010-web-session-interface, US4;
5
+ # 011-web-adopt).
6
+ #
7
+ # Self-healing permissions model ("start as root, fix perms, drop
8
+ # privileges"). The image starts as root (no `USER remo` in the Dockerfile)
9
+ # so this script can, when it is UID 0, repair ownership on:
10
+ #
11
+ # * the config dir ($REMO_HOME, default $HOME/.config/remo) — a deployer
12
+ # that BIND-MOUNTS this dir gets it owned root:root, so the non-root
13
+ # `remo` process could not create web-identity/ there (adopted mode,
14
+ # 011-web-adopt) and would fail with EACCES on first boot. Docker seeds a
15
+ # fresh NAMED volume root-owned too, so the same fix covers that path.
16
+ # * the SSH ControlMaster runtime dir (/run/remo-ssh) — an option-less
17
+ # tmpfs is remounted root-owned 0755 (unwritable to `remo`) by a container
18
+ # RESTART, which otherwise fails the startup runtime-dir check on every
19
+ # boot after the first. Re-healing it on EVERY start makes restarts behave
20
+ # like the first boot without the deployer pinning `rw,mode=1777`.
21
+ #
22
+ # After healing, it drops to the unprivileged `remo` user via `gosu` and
23
+ # re-execs itself; that second pass takes the non-root branch below (the gate
24
+ # + serve). `gosu` keeps us as PID 1 with correct signal semantics (SIGTERM
25
+ # reaches uvicorn for graceful shutdown) and, unlike a setuid `su`, adds no
26
+ # privilege-escalation surface — compatible with `no-new-privileges:true`.
27
+ #
28
+ # Healing is BEST-EFFORT and never hard-fails startup: a read-only config
29
+ # mount (mount-configured mode) legitimately cannot be chowned, and a deployer
30
+ # may drop CAP_CHOWN. `remo web check` (below) is the real gate on whether the
31
+ # resulting filesystem is actually usable. If the container is started with an
32
+ # explicit non-root `--user`/`user:`, this script is never UID 0, so it skips
33
+ # healing entirely and just runs the gate + serve.
34
+ #
35
+ # The `remo web check --skip-instance-checks` gate runs unguarded under
36
+ # `set -e`: a non-zero exit from the CONFIG gate aborts before `exec remo web
37
+ # serve`, so the container fails fast on genuinely bad config/mounts and never
38
+ # starts serving. `--skip-instance-checks` is deliberate: the startup gate
39
+ # validates only config/mounts/executables, NOT per-instance reachability. A
40
+ # registered instance that is merely powered off or unreachable must NOT
41
+ # prevent the whole service from starting (FR-006/US1); under `restart:
42
+ # unless-stopped` (compose.example.yml), gating on reachability would
43
+ # crash-loop the container whenever any single instance is down. Full
44
+ # per-instance diagnostics remain available via `remo web check` (no flag),
45
+ # and readiness is (correctly) config-only via GET /api/v1/ready.
46
+ #
47
+ # 011-web-adopt (SC-006): *unconfigured* is a PASSING state for this gate. A
48
+ # fresh writable state volume with no registry/key material yet makes `remo
49
+ # web check` report "awaiting adoption" and exit 0, so an adopted-mode
50
+ # container boots cleanly (no crash loop) while it waits for adoption. Only
51
+ # genuinely broken configuration still aborts here.
52
+
53
+ set -euo pipefail
54
+
55
+ # Target uid/gid to drop to. Defaults to the image's baked-in `remo` user
56
+ # (1000:1000); override PUID/PGID for a bind-mount host whose directory owner
57
+ # is some other uid/gid.
58
+ PUID="${PUID:-1000}"
59
+ PGID="${PGID:-1000}"
60
+
61
+ # Paths that must be writable by the dropped-to user. CONFIG_DIR mirrors
62
+ # core/config.py's REMO_HOME resolution; CONTROL_DIR mirrors
63
+ # WebSettings.ssh_control_dir (REMO_WEB_SSH_CONTROL_DIR, default /run/remo-ssh).
64
+ CONFIG_DIR="${REMO_HOME:-${HOME:-/home/remo}/.config/remo}"
65
+ CONTROL_DIR="${REMO_WEB_SSH_CONTROL_DIR:-/run/remo-ssh}"
66
+
67
+ if [ "$(id -u)" -eq 0 ]; then
68
+ # Root (the image default): self-heal, then drop. Every step is
69
+ # best-effort (`|| true`) so a read-only mount or a dropped CAP_CHOWN can
70
+ # never abort startup — `remo web check` below is the authority.
71
+ mkdir -p "$CONFIG_DIR" 2>/dev/null || true
72
+ mkdir -p "$CONTROL_DIR" 2>/dev/null || true
73
+ chown -R "$PUID:$PGID" "$CONFIG_DIR" 2>/dev/null || true
74
+ # chmod BEFORE chown so it runs while root still owns the tmpfs (no
75
+ # CAP_FOWNER needed). 1777 (sticky, world-writable) already lets the
76
+ # dropped-to user create control sockets, so the chown that follows is
77
+ # belt-and-suspenders — harmless if it can't change ownership.
78
+ chmod 1777 "$CONTROL_DIR" 2>/dev/null || true
79
+ chown "$PUID:$PGID" "$CONTROL_DIR" 2>/dev/null || true
80
+ # Re-exec this same script as the unprivileged user; the second pass falls
81
+ # through to the gate + serve below.
82
+ exec gosu "$PUID:$PGID" "$0" "$@"
83
+ fi
84
+
85
+ # Unprivileged: either dropped from root above, or started with an explicit
86
+ # non-root `user:` (healing skipped, by design).
87
+ remo web check --skip-instance-checks
88
+ exec remo web serve
@@ -358,7 +358,7 @@ tooling.
358
358
  | Flag | Value | Why |
359
359
  |---|---|---|
360
360
  | `read_only` | `true` | The image ships no legitimate reason to write to its own filesystem at runtime — all mutable state is either ephemeral (PTYs, SSH ControlMaster sockets) or lives in the explicit `tmpfs` mount. A read-only rootfs means a compromised process can't persist a backdoor into the image layer. |
361
- | `user: "1000:1000"` | non-root UID/GID | The image creates a dedicated `remo` user (UID 1000) at build time and drops root via `USER remo` in the Dockerfile before the entrypoint runs. Running as non-root limits what a container-escape or dependency-confusion bug can do to the host. |
361
+ | non-root `remo` (UID 1000) | non-root process | The app always ends up running as the dedicated `remo` user (UID 1000). In the read-only bind-mount service the container is pinned to `user: "1000:1000"`; the adopted-mode service instead starts as **root** and its entrypoint drops to `remo` via `gosu` after self-healing filesystem permissions (chowning a root-owned bind-mounted/named-volume config dir and re-healing the `/run/remo-ssh` tmpfs a restart remounts root-owned). Either way the serving process is non-root, which limits what a container-escape or dependency-confusion bug can do to the host. The adopted service therefore grants back only the minimal capabilities the heal-then-drop needs (`CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID`) on top of `cap_drop: ALL`; the dropped-to `remo` process holds none of them effectively. |
362
362
  | `security_opt: no-new-privileges:true` | — | Prevents any process in the container (including a compromised one) from gaining privileges via setuid/setgid binaries, closing off a common container-escape vector. |
363
363
  | `cap_drop: ALL` | — | The service needs no Linux capabilities at all (it doesn't bind privileged ports, doesn't need `ptrace`, doesn't need raw sockets) — dropping everything removes capabilities an attacker could otherwise abuse. |
364
364
  | `restart: unless-stopped` | — | Keeps the service available across host reboots/crashes without fighting an operator's deliberate `docker compose stop`. |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "remo-cli"
7
- version = "2.2.0rc2"
7
+ version = "2.2.0rc3"
8
8
  description = "Remote development environment CLI"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -45,6 +45,16 @@ within 30s, generates a persistent service identity (same fingerprint across
45
45
  a container restart, FR-002), and token-gates the setup API (404 when
46
46
  `REMO_WEB_API_TOKEN` is unset, FR-021). The pre-existing RO-mount tests
47
47
  above are intentionally untouched — them still passing IS SC-005.
48
+
49
+ A fourth group covers the self-healing permissions model: the image starts
50
+ as root and its entrypoint chowns a ROOT-OWNED (bind-mounted) config dir and
51
+ re-heals the /run/remo-ssh tmpfs on every start, then drops to the non-root
52
+ `remo` user via gosu. These tests start the container with a root-owned bind
53
+ dir, an option-less `--tmpfs /run/remo-ssh` (no `rw,mode=1777` pin), a
54
+ read-only rootfs, `no-new-privileges`, and only the heal-then-drop caps, then
55
+ assert: boot reaches `unconfigured`, web-identity/ is generated owned by
56
+ `remo`, the app process (PID 1) runs as uid 1000 (not root), and a
57
+ `docker restart` keeps the same identity with a still-writable tmpfs.
48
58
  """
49
59
 
50
60
  from __future__ import annotations
@@ -126,11 +136,53 @@ def test_dockerfile_sets_frontend_dist_dir_override():
126
136
  assert dist_path in text.replace(dist_env_line, "") # referenced by a COPY --from= too
127
137
 
128
138
 
129
- def test_dockerfile_runs_as_non_root():
139
+ def test_dockerfile_starts_as_root_for_self_healing_entrypoint():
140
+ """The image must NOT hard-pin `USER remo` — it starts as root so the
141
+ entrypoint can self-heal ownership on a bind-mounted, root-owned config
142
+ dir and on the /run/remo-ssh tmpfs before dropping privileges. The drop
143
+ to non-root now happens at runtime (via gosu in docker/entrypoint.sh),
144
+ not via a build-time USER line."""
130
145
  text = DOCKERFILE.read_text()
131
- assert "USER remo" in text
132
- # USER remo must come after the package/asset installs (root-only ops).
133
- assert text.index("USER remo") > text.index("useradd")
146
+ # No active `USER remo` directive (a commented mention is fine).
147
+ assert not any(
148
+ line.strip().startswith("USER remo") for line in text.splitlines()
149
+ ), "Dockerfile must start as root; the entrypoint drops to `remo` via gosu"
150
+ # gosu must be installed for the entrypoint's privilege drop.
151
+ assert "gosu" in text
152
+ # The remo user must still be created (root-only op) for gosu to drop to.
153
+ assert "useradd" in text
154
+ # HOME is pinned so config-path resolution matches across the root heal
155
+ # pass and the dropped-to `remo` process.
156
+ assert "ENV HOME=/home/remo" in text
157
+
158
+
159
+ def test_entrypoint_self_heals_as_root_then_drops_via_gosu():
160
+ """Root branch: mkdir/chown the config dir and the SSH control tmpfs, then
161
+ `exec gosu` to the unprivileged user (PID 1 / signal semantics preserved)."""
162
+ text = ENTRYPOINT.read_text()
163
+ # Guarded on being UID 0 so an explicit non-root `--user` skips healing.
164
+ assert "id -u" in text
165
+ assert "chown" in text
166
+ # The drop is an exec-form gosu (never backgrounded).
167
+ assert "exec gosu" in text
168
+ # It heals both the config dir and the SSH control (runtime) dir.
169
+ assert "CONFIG_DIR" in text
170
+ assert "CONTROL_DIR" in text
171
+
172
+
173
+ def test_entrypoint_healing_is_best_effort():
174
+ """Healing must never hard-fail startup (a read-only config mount can't be
175
+ chowned; a deployer may drop CAP_CHOWN) — `remo web check` is the real
176
+ gate. Every mkdir/chown/chmod step tolerates failure."""
177
+ text = ENTRYPOINT.read_text()
178
+ heal_lines = [
179
+ ln.strip()
180
+ for ln in text.splitlines()
181
+ if ln.strip().startswith(("mkdir", "chown", "chmod"))
182
+ ]
183
+ assert heal_lines, "expected filesystem-healing lines in the entrypoint"
184
+ for line in heal_lines:
185
+ assert "|| true" in line or "2>/dev/null" in line, line
134
186
 
135
187
 
136
188
  def test_dockerfile_entrypoint_is_the_finalized_script():
@@ -216,6 +268,30 @@ def test_compose_mounts_registry_and_ssh_material_readonly():
216
268
  assert all(v.endswith(":ro") for v in volumes if not v.strip().startswith("#"))
217
269
 
218
270
 
271
+ def test_compose_adopted_service_self_heals_without_workarounds():
272
+ """The adopted-mode service must rely on the entrypoint's self-healing,
273
+ not on deployer workarounds: a PLAIN tmpfs (no `rw,mode=1777` pin) and no
274
+ `user:` pin (so it starts as root and heals), while keeping the read-only
275
+ rootfs + no-new-privileges posture and granting only the capabilities the
276
+ heal-then-drop needs."""
277
+ svc = _load_compose()["services"]["remo-web-adopted"]
278
+ # Plain, option-less tmpfs — the entrypoint re-heals it on every start.
279
+ assert "/run/remo-ssh" in svc["tmpfs"]
280
+ assert not any("mode=" in t for t in svc["tmpfs"]), (
281
+ "the rw,mode=1777 tmpfs pin should be gone — the entrypoint re-heals "
282
+ "/run/remo-ssh on restart"
283
+ )
284
+ # No `user:` pin: starts as root so the entrypoint can heal the state
285
+ # volume before dropping to `remo`.
286
+ assert "user" not in svc
287
+ # Hardening preserved; caps are drop-ALL + only the heal-then-drop set.
288
+ assert svc["read_only"] is True
289
+ assert "no-new-privileges:true" in svc["security_opt"]
290
+ assert svc["cap_drop"] == ["ALL"]
291
+ for cap in ("CHOWN", "SETUID", "SETGID"):
292
+ assert cap in svc["cap_add"], cap
293
+
294
+
219
295
  @pytest.mark.skipif(
220
296
  shutil.which("docker") is None, reason="docker compose config requires the docker CLI"
221
297
  )
@@ -913,6 +989,180 @@ def test_amd64_setup_routes_hidden_without_token_env(amd64_image, empty_state_di
913
989
  _docker_rm(name)
914
990
 
915
991
 
992
+ # ---------------------------------------------------------------------------
993
+ # Tier 2, group 4: self-healing permissions (root-start heal -> drop). Same
994
+ # opt-in gate and hardening flags, plus the heal-then-drop capability set.
995
+ # ---------------------------------------------------------------------------
996
+
997
+ # cap_drop ALL + only what the heal-then-drop entrypoint needs, mirroring the
998
+ # `remo-web-adopted` service in compose.example.yml.
999
+ _HEAL_RUN_FLAGS = [
1000
+ "--read-only",
1001
+ "--cap-drop", "ALL",
1002
+ "--cap-add", "CHOWN",
1003
+ "--cap-add", "DAC_OVERRIDE",
1004
+ "--cap-add", "FOWNER",
1005
+ "--cap-add", "SETUID",
1006
+ "--cap-add", "SETGID",
1007
+ "--security-opt", "no-new-privileges:true",
1008
+ # Option-less tmpfs (NO rw,mode=1777 pin): the entrypoint re-heals it.
1009
+ "--tmpfs", "/run/remo-ssh",
1010
+ ]
1011
+
1012
+
1013
+ @pytest.fixture
1014
+ def root_owned_state_dir(tmp_path, amd64_image):
1015
+ """A root:root 0755 dir bind-mounted at the container's REMO_HOME.
1016
+
1017
+ Reproduces the bind-mount-from-an-app-platform case the self-healing
1018
+ entrypoint exists for: a non-root process cannot create web-identity/
1019
+ there. Ownership is set (and later cleaned up) via a throwaway root
1020
+ container so the test needs no host root — the same technique
1021
+ `empty_state_dir` uses for cleanup.
1022
+ """
1023
+ state_dir = tmp_path / "rootstate"
1024
+ state_dir.mkdir()
1025
+
1026
+ def _root_sh(script: str, check: bool) -> None:
1027
+ subprocess.run(
1028
+ [
1029
+ "docker", "run", "--rm", "--user", "0:0", "--entrypoint", "sh",
1030
+ "-v", f"{state_dir}:/state", amd64_image, "-c", script,
1031
+ ],
1032
+ check=check,
1033
+ capture_output=True,
1034
+ timeout=30,
1035
+ )
1036
+
1037
+ _root_sh("chown 0:0 /state && chmod 0755 /state", check=True)
1038
+ yield state_dir
1039
+ _root_sh("rm -rf /state/web-identity /state/known_hosts", check=False)
1040
+
1041
+
1042
+ def _proc1_uid(name: str) -> str:
1043
+ """The real (numeric) uid of PID 1 inside the container, read from
1044
+ /proc/1/status (python:3.11-slim ships no `ps`)."""
1045
+ result = subprocess.run(
1046
+ ["docker", "exec", name, "cat", "/proc/1/status"],
1047
+ capture_output=True,
1048
+ text=True,
1049
+ timeout=10,
1050
+ )
1051
+ assert result.returncode == 0, result.stdout + result.stderr
1052
+ for line in result.stdout.splitlines():
1053
+ if line.startswith("Uid:"):
1054
+ # "Uid:\treal\teffective\tsaved\tfilesystem"
1055
+ return line.split()[1]
1056
+ raise AssertionError(f"no Uid line in /proc/1/status:\n{result.stdout}")
1057
+
1058
+
1059
+ @requires_image_build
1060
+ def test_amd64_root_owned_bind_dir_self_heals_and_app_runs_non_root(
1061
+ amd64_image, root_owned_state_dir
1062
+ ):
1063
+ """Acceptance: a root-owned bind dir + read-only rootfs + option-less
1064
+ tmpfs boots to awaiting-adoption, generates web-identity/ OWNED BY remo,
1065
+ and the app process (PID 1) runs as the non-root `remo` user (uid 1000),
1066
+ not root — the entrypoint healed as root then dropped via gosu."""
1067
+ name = f"remo-web-test-heal-{uuid.uuid4().hex[:8]}"
1068
+ try:
1069
+ cmd = ["docker", "run", "-d", "--name", name]
1070
+ cmd += _HEAL_RUN_FLAGS
1071
+ cmd += [
1072
+ "-v", f"{root_owned_state_dir}:{_CONTAINER_REMO_HOME}",
1073
+ "-e", f"REMO_WEB_API_TOKEN={_SETUP_TOKEN}",
1074
+ amd64_image,
1075
+ ]
1076
+ run = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
1077
+ assert run.returncode == 0, run.stderr
1078
+
1079
+ assert _wait_for_ready_payload(name)["status"] == "unconfigured"
1080
+
1081
+ # The app must run as non-root remo despite starting from root.
1082
+ assert _proc1_uid(name) == "1000"
1083
+
1084
+ # web-identity/ was generated and is owned by remo (uid 1000), proving
1085
+ # the root-side chown healed the root-owned bind dir.
1086
+ owner = subprocess.run(
1087
+ ["docker", "exec", name, "stat", "-c", "%u", f"{_IDENTITY_DIR}/id_ed25519"],
1088
+ capture_output=True,
1089
+ text=True,
1090
+ timeout=10,
1091
+ )
1092
+ assert owner.returncode == 0, owner.stdout + owner.stderr
1093
+ assert owner.stdout.strip() == "1000", owner.stdout
1094
+ finally:
1095
+ _docker_rm(name)
1096
+
1097
+
1098
+ @requires_image_build
1099
+ def test_amd64_bare_tmpfs_survives_restart_via_reheal(amd64_image, root_owned_state_dir):
1100
+ """Acceptance: with an OPTION-LESS `--tmpfs /run/remo-ssh` (no
1101
+ rw,mode=1777 pin), a `docker restart` — which remounts the tmpfs
1102
+ root-owned 0755 — still comes back `unconfigured` with the SAME identity
1103
+ and a still-writable control dir, because the entrypoint re-heals the
1104
+ tmpfs on every start."""
1105
+ name = f"remo-web-test-reheal-{uuid.uuid4().hex[:8]}"
1106
+ try:
1107
+ cmd = ["docker", "run", "-d", "--name", name]
1108
+ cmd += _HEAL_RUN_FLAGS
1109
+ cmd += [
1110
+ "-v", f"{root_owned_state_dir}:{_CONTAINER_REMO_HOME}",
1111
+ "-e", f"REMO_WEB_API_TOKEN={_SETUP_TOKEN}",
1112
+ amd64_image,
1113
+ ]
1114
+ run = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
1115
+ assert run.returncode == 0, run.stderr
1116
+
1117
+ assert _wait_for_ready_payload(name)["status"] == "unconfigured"
1118
+ fingerprint_before = _service_key_fingerprint(name)
1119
+
1120
+ restart = subprocess.run(
1121
+ ["docker", "restart", name], capture_output=True, text=True, timeout=60
1122
+ )
1123
+ assert restart.returncode == 0, restart.stderr
1124
+
1125
+ # Ready again (the re-heal made the restart-remounted tmpfs writable),
1126
+ # same identity (FR-002).
1127
+ assert _wait_for_ready_payload(name)["status"] == "unconfigured"
1128
+ assert _service_key_fingerprint(name) == fingerprint_before
1129
+
1130
+ # The control dir is writable by the dropped-to remo user after the
1131
+ # restart re-heal.
1132
+ probe = subprocess.run(
1133
+ ["docker", "exec", "--user", "1000:1000", name, "touch", "/run/remo-ssh/probe"],
1134
+ capture_output=True,
1135
+ text=True,
1136
+ timeout=10,
1137
+ )
1138
+ assert probe.returncode == 0, probe.stdout + probe.stderr
1139
+ finally:
1140
+ _docker_rm(name)
1141
+
1142
+
1143
+ @requires_image_build
1144
+ def test_amd64_explicit_non_root_user_still_boots(amd64_image, empty_state_dir):
1145
+ """A deployer that pins an explicit non-root `--user` starts non-root, so
1146
+ the entrypoint skips healing (best-effort) and execs directly. With a
1147
+ writable state dir it must still boot to `unconfigured` — healing being
1148
+ skipped must never be fatal."""
1149
+ name = f"remo-web-test-explicituser-{uuid.uuid4().hex[:8]}"
1150
+ try:
1151
+ cmd = ["docker", "run", "-d", "--name", name, "--user", "1000:1000"]
1152
+ cmd += _HEAL_RUN_FLAGS
1153
+ cmd += [
1154
+ "-v", f"{empty_state_dir}:{_CONTAINER_REMO_HOME}",
1155
+ "-e", f"REMO_WEB_API_TOKEN={_SETUP_TOKEN}",
1156
+ amd64_image,
1157
+ ]
1158
+ run = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
1159
+ assert run.returncode == 0, run.stderr
1160
+ assert _wait_for_ready_payload(name)["status"] == "unconfigured"
1161
+ assert _proc1_uid(name) == "1000"
1162
+ finally:
1163
+ _docker_rm(name)
1164
+
1165
+
916
1166
  # ---------------------------------------------------------------------------
917
1167
  # Helpers
918
1168
  # ---------------------------------------------------------------------------
@@ -1038,7 +1038,7 @@ wheels = [
1038
1038
 
1039
1039
  [[package]]
1040
1040
  name = "remo-cli"
1041
- version = "2.2.0rc1"
1041
+ version = "2.2.0rc3"
1042
1042
  source = { editable = "." }
1043
1043
  dependencies = [
1044
1044
  { name = "ansible-core" },
@@ -1,37 +0,0 @@
1
- #!/usr/bin/env bash
2
- # docker/entrypoint.sh
3
- #
4
- # Container entrypoint for `remo web` (010-web-session-interface, US4).
5
- # Non-root-safe: runs as whatever user the container was started as
6
- # (see compose.example.yml `user: "1000:1000"`), no root-only operations.
7
- #
8
- # `set -euo pipefail` + running `remo web check --skip-instance-checks`
9
- # unguarded means a non-zero exit from the CONFIG gate aborts this script
10
- # before `exec remo web serve` is ever reached — i.e. the container fails
11
- # fast and never starts serving on bad config/missing mounts.
12
- #
13
- # `--skip-instance-checks` is deliberate: the startup gate validates only
14
- # config/mounts/executables, NOT per-instance reachability. A registered
15
- # instance that is merely powered off or unreachable must NOT prevent the
16
- # whole service from starting (FR-006/US1 — unreachable instances stay
17
- # visible with actionable status; they don't block boot). Under
18
- # `restart: unless-stopped` (compose.example.yml), gating on reachability
19
- # would crash-loop the container whenever any single instance is down.
20
- # Full per-instance diagnostics remain available via `remo web check` (no
21
- # flag), and readiness is (correctly) config-only via GET /api/v1/ready.
22
- #
23
- # 011-web-adopt (SC-006): *unconfigured* is a PASSING state for this gate.
24
- # A fresh writable state volume with no registry/key material yet makes
25
- # `remo web check` report "awaiting adoption — run `remo web adopt`" and
26
- # exit 0, so an adopted-mode container boots cleanly (no crash loop under
27
- # `restart: unless-stopped`) while it waits for adoption. Only genuinely
28
- # broken configuration (mounted artifacts present but unusable, missing
29
- # runtime prerequisites) still aborts here.
30
- #
31
- # `exec` replaces this script's process (PID 1) with `remo web serve` so it
32
- # receives signals (SIGTERM) directly, which graceful shutdown depends on.
33
-
34
- set -euo pipefail
35
-
36
- remo web check --skip-instance-checks
37
- exec remo web serve
File without changes
File without changes