remo-cli 2.2.0rc2__tar.gz → 2.2.0rc4__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 (439) hide show
  1. remo_cli-2.2.0rc4/.specify/feature.json +3 -0
  2. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/PKG-INFO +1 -1
  3. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docker/Dockerfile +26 -7
  4. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docker/compose.example.yml +70 -29
  5. remo_cli-2.2.0rc4/docker/entrypoint.sh +88 -0
  6. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/web-session-interface.md +107 -89
  7. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/api/client.ts +75 -0
  8. remo_cli-2.2.0rc4/frontend/src/components/AwaitingAdoption.tsx +148 -0
  9. remo_cli-2.2.0rc4/frontend/src/components/PairToSync.css +53 -0
  10. remo_cli-2.2.0rc4/frontend/src/components/PairToSync.tsx +110 -0
  11. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/TopBar.tsx +3 -0
  12. remo_cli-2.2.0rc4/frontend/src/lib/clipboard.ts +28 -0
  13. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/pyproject.toml +1 -1
  14. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/contracts/cli-web-adopt.md +63 -0
  15. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/contracts/pairing-api.md +72 -0
  16. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/contracts/setup-api.md +52 -0
  17. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/data-model.md +102 -0
  18. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/plan.md +167 -0
  19. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/quickstart.md +132 -0
  20. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/research.md +287 -0
  21. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/spec.md +435 -0
  22. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/tasks.md +239 -0
  23. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/web.py +79 -54
  24. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/web_adopt.py +191 -216
  25. remo_cli-2.2.0rc4/src/remo_cli/web/api/pairing.py +81 -0
  26. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/setup.py +49 -31
  27. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/app.py +39 -0
  28. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/check.py +45 -0
  29. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/config.py +24 -4
  30. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/health.py +23 -0
  31. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/logging_config.py +8 -0
  32. remo_cli-2.2.0rc4/src/remo_cli/web/operator_auth.py +109 -0
  33. remo_cli-2.2.0rc4/src/remo_cli/web/pairing.py +142 -0
  34. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/image/test_docker_image.py +301 -27
  35. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_web_adopt_e2e.py +90 -155
  36. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/test_web_adopt_cmd.py +18 -19
  37. remo_cli-2.2.0rc4/tests/unit/core/test_web_adopt_code.py +56 -0
  38. remo_cli-2.2.0rc4/tests/unit/core/test_web_push.py +386 -0
  39. remo_cli-2.2.0rc4/tests/unit/web/_pairing_support.py +53 -0
  40. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_csp.py +6 -4
  41. remo_cli-2.2.0rc4/tests/unit/web/test_mint_gating.py +57 -0
  42. remo_cli-2.2.0rc4/tests/unit/web/test_operator_auth.py +90 -0
  43. remo_cli-2.2.0rc4/tests/unit/web/test_pairing.py +116 -0
  44. remo_cli-2.2.0rc4/tests/unit/web/test_pairing_origin.py +43 -0
  45. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_setup_api.py +34 -12
  46. remo_cli-2.2.0rc4/tests/unit/web/test_setup_auth.py +273 -0
  47. remo_cli-2.2.0rc4/tests/unit/web/test_setup_dormancy.py +110 -0
  48. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_state.py +11 -5
  49. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/uv.lock +1 -1
  50. remo_cli-2.2.0rc2/.specify/feature.json +0 -3
  51. remo_cli-2.2.0rc2/docker/entrypoint.sh +0 -37
  52. remo_cli-2.2.0rc2/frontend/src/components/AwaitingAdoption.tsx +0 -109
  53. remo_cli-2.2.0rc2/tests/unit/core/test_web_push.py +0 -630
  54. remo_cli-2.2.0rc2/tests/unit/web/test_setup_auth.py +0 -378
  55. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-analyze/SKILL.md +0 -0
  56. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-checklist/SKILL.md +0 -0
  57. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-clarify/SKILL.md +0 -0
  58. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-constitution/SKILL.md +0 -0
  59. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-converge/SKILL.md +0 -0
  60. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
  61. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
  62. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
  63. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
  64. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
  65. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-implement/SKILL.md +0 -0
  66. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-plan/SKILL.md +0 -0
  67. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-specify/SKILL.md +0 -0
  68. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-tasks/SKILL.md +0 -0
  69. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
  70. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.devcontainer/devcontainer.json +0 -0
  71. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.dockerignore +0 -0
  72. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/dependabot.yml +0 -0
  73. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/workflows/ci.yml +0 -0
  74. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/workflows/provision.yml +0 -0
  75. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/workflows/release.yml +0 -0
  76. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/workflows/smoke-test-cleanup.yml +0 -0
  77. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/workflows/smoke-test.yml +0 -0
  78. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.github/workflows/teardown.yml +0 -0
  79. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.gitignore +0 -0
  80. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
  81. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
  82. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.maverick/checkpoints/feature/validation_complete.json +0 -0
  83. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/.registry +0 -0
  84. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/README.md +0 -0
  85. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
  86. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
  87. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
  88. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
  89. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
  90. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/config-template.yml +0 -0
  91. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/extension.yml +0 -0
  92. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/git-config.yml +0 -0
  93. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
  94. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
  95. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
  96. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
  97. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
  98. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
  99. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
  100. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
  101. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/extensions.yml +0 -0
  102. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/init-options.json +0 -0
  103. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/integration.json +0 -0
  104. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/integrations/claude.manifest.json +0 -0
  105. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/integrations/speckit.manifest.json +0 -0
  106. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/memory/constitution.md +0 -0
  107. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  108. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/scripts/bash/common.sh +0 -0
  109. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/scripts/bash/create-new-feature.sh +0 -0
  110. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/scripts/bash/setup-plan.sh +0 -0
  111. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/scripts/bash/setup-tasks.sh +0 -0
  112. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/scripts/bash/update-agent-context.sh +0 -0
  113. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/templates/agent-file-template.md +0 -0
  114. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/templates/checklist-template.md +0 -0
  115. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/templates/constitution-template.md +0 -0
  116. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/templates/plan-template.md +0 -0
  117. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/templates/spec-template.md +0 -0
  118. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/templates/tasks-template.md +0 -0
  119. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/workflows/speckit/workflow.yml +0 -0
  120. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/.specify/workflows/workflow-registry.json +0 -0
  121. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/AGENTS.md +0 -0
  122. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/CLAUDE.md +0 -0
  123. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/CONTRIBUTING.md +0 -0
  124. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/Dockerfile +0 -0
  125. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/LICENSE +0 -0
  126. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/README.md +0 -0
  127. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/.ansible-lint +0 -0
  128. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/README.md +0 -0
  129. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/ansible.cfg +0 -0
  130. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/aws_configure.yml +0 -0
  131. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/aws_resize.yml +0 -0
  132. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/aws_site.yml +0 -0
  133. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/aws_teardown.yml +0 -0
  134. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/group_vars/all.yml +0 -0
  135. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/group_vars/incus_containers.yml +0 -0
  136. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/group_vars/proxmox_containers.yml +0 -0
  137. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/hetzner_configure.yml +0 -0
  138. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/hetzner_provision.yml +0 -0
  139. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/hetzner_resize.yml +0 -0
  140. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/hetzner_site.yml +0 -0
  141. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/hetzner_teardown.yml +0 -0
  142. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/incus_bootstrap.yml +0 -0
  143. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/incus_configure.yml +0 -0
  144. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/incus_provision.yml +0 -0
  145. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/incus_resize.yml +0 -0
  146. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/incus_site.yml +0 -0
  147. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/incus_teardown.yml +0 -0
  148. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/inventory/hosts.yml +0 -0
  149. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/inventory/incus_containers.yml +0 -0
  150. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/proxmox_bootstrap.yml +0 -0
  151. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/proxmox_configure.yml +0 -0
  152. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/proxmox_provision.yml +0 -0
  153. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/proxmox_resize.yml +0 -0
  154. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/proxmox_site.yml +0 -0
  155. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/proxmox_teardown.yml +0 -0
  156. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/requirements.yml +0 -0
  157. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/defaults/main.yml +0 -0
  158. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
  159. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
  160. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/main.yml +0 -0
  161. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
  162. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
  163. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
  164. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/deacon/defaults/main.yml +0 -0
  165. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/deacon/tasks/main.yml +0 -0
  166. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/devcontainers/tasks/main.yml +0 -0
  167. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/docker/handlers/main.yml +0 -0
  168. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/docker/tasks/main.yml +0 -0
  169. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/fzf/tasks/main.yml +0 -0
  170. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/github_cli/tasks/main.yml +0 -0
  171. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
  172. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
  173. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
  174. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
  175. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
  176. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/defaults/main.yml +0 -0
  177. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/handlers/main.yml +0 -0
  178. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/meta/main.yml +0 -0
  179. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/tasks/main.yml +0 -0
  180. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
  181. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
  182. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
  183. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
  184. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/nodejs/defaults/main.yml +0 -0
  185. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/nodejs/tasks/main.yml +0 -0
  186. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
  187. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
  188. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
  189. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
  190. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
  191. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/meta/main.yml +0 -0
  192. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
  193. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
  194. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
  195. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
  196. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
  197. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/defaults/main.yml +0 -0
  198. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/tasks/main.yml +0 -0
  199. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
  200. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/project-launch.sh.j2 +0 -0
  201. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
  202. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/remo-host.sh.j2 +0 -0
  203. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/zellij/defaults/main.yml +0 -0
  204. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/zellij/tasks/main.yml +0 -0
  205. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
  206. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
  207. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/ansible/tasks/configure_dev_tools.yml +0 -0
  208. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/aws.md +0 -0
  209. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/hetzner.md +0 -0
  210. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/incus.md +0 -0
  211. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/install.sh +0 -0
  212. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/proxmox.md +0 -0
  213. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/remo-web-speckit-input.md +0 -0
  214. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/docs/remo-web.html +0 -0
  215. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/index.html +0 -0
  216. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/package-lock.json +0 -0
  217. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/package.json +0 -0
  218. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/public/README.md +0 -0
  219. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/public/dist__ghostty-vt.wasm +0 -0
  220. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/public/ghostty-vt.wasm +0 -0
  221. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/scripts/copy-ghostty-wasm.mjs +0 -0
  222. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/AppRoot.tsx +0 -0
  223. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/AppShell.css +0 -0
  224. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/AppShell.tsx +0 -0
  225. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/AwaitingAdoption.css +0 -0
  226. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/OfflineOverlay.css +0 -0
  227. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/OfflineOverlay.tsx +0 -0
  228. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/SessionRail.css +0 -0
  229. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/SessionRail.tsx +0 -0
  230. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/SettingsPage.css +0 -0
  231. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/SettingsPage.tsx +0 -0
  232. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/ShortcutsModal.css +0 -0
  233. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/ShortcutsModal.tsx +0 -0
  234. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/TerminalCard.css +0 -0
  235. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/TerminalCard.tsx +0 -0
  236. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/TopBar.css +0 -0
  237. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/WorkspacePane.css +0 -0
  238. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/WorkspacePane.tsx +0 -0
  239. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/providerMeta.ts +0 -0
  240. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/components/railModel.ts +0 -0
  241. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/main.tsx +0 -0
  242. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/state/discovery.ts +0 -0
  243. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/state/fonts.ts +0 -0
  244. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/state/health.ts +0 -0
  245. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/state/settings.ts +0 -0
  246. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/state/useConsoleKeyboard.ts +0 -0
  247. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/state/workspace.ts +0 -0
  248. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/terminal/.gitkeep +0 -0
  249. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/terminal/GhosttyRenderer.ts +0 -0
  250. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/terminal/RendererAdapter.ts +0 -0
  251. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/terminal/TerminalConnection.ts +0 -0
  252. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/terminal/XtermRenderer.ts +0 -0
  253. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/terminal/defaultRenderer.ts +0 -0
  254. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/theme/fonts.ts +0 -0
  255. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/src/theme/tokens.css +0 -0
  256. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/tsconfig.json +0 -0
  257. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/frontend/vite.config.ts +0 -0
  258. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/install.sh +0 -0
  259. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/maverick.yaml +0 -0
  260. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/playwright.config.ts +0 -0
  261. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/project-menu.sh +0 -0
  262. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/remo +0 -0
  263. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/run.sh +0 -0
  264. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/scripts/setup-aws-oidc.sh +0 -0
  265. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
  266. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
  267. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
  268. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
  269. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/data-model.md +0 -0
  270. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/plan.md +0 -0
  271. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
  272. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/research.md +0 -0
  273. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/spec.md +0 -0
  274. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/tasks.md +0 -0
  275. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/checklists/requirements.md +0 -0
  276. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
  277. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
  278. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/data-model.md +0 -0
  279. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/plan.md +0 -0
  280. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/quickstart.md +0 -0
  281. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/research.md +0 -0
  282. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/spec.md +0 -0
  283. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/tasks.md +0 -0
  284. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
  285. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/data-model.md +0 -0
  286. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/plan.md +0 -0
  287. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/quickstart.md +0 -0
  288. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/research.md +0 -0
  289. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/spec.md +0 -0
  290. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/tasks.md +0 -0
  291. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/004-proxmox-container-support/spec.md +0 -0
  292. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/checklists/requirements.md +0 -0
  293. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/contracts/cli-surface.md +0 -0
  294. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/data-model.md +0 -0
  295. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/plan.md +0 -0
  296. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/quickstart.md +0 -0
  297. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/research.md +0 -0
  298. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/spec.md +0 -0
  299. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/tasks.md +0 -0
  300. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/checklists/requirements.md +0 -0
  301. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/contracts/remo-host-protocol.md +0 -0
  302. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/contracts/rest-api.md +0 -0
  303. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/contracts/terminal-websocket.md +0 -0
  304. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/data-model.md +0 -0
  305. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/plan.md +0 -0
  306. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/quickstart.md +0 -0
  307. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/research.md +0 -0
  308. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/spec.md +0 -0
  309. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/tasks.md +0 -0
  310. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/checklists/requirements.md +0 -0
  311. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/contracts/cli-web-adopt.md +0 -0
  312. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/contracts/setup-api.md +0 -0
  313. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/data-model.md +0 -0
  314. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/plan.md +0 -0
  315. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/quickstart-results.md +0 -0
  316. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/quickstart.md +0 -0
  317. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/research.md +0 -0
  318. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/spec.md +0 -0
  319. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/specs/011-web-adopt/tasks.md +0 -0
  320. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/__init__.py +0 -0
  321. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/__main__.py +0 -0
  322. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/__init__.py +0 -0
  323. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/cp.py +0 -0
  324. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/main.py +0 -0
  325. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/__init__.py +0 -0
  326. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/aws.py +0 -0
  327. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/hetzner.py +0 -0
  328. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/incus.py +0 -0
  329. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/proxmox.py +0 -0
  330. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/cli/shell.py +0 -0
  331. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/__init__.py +0 -0
  332. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/ansible_runner.py +0 -0
  333. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/completion.py +0 -0
  334. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/config.py +0 -0
  335. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/known_hosts.py +0 -0
  336. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/output.py +0 -0
  337. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/picker.py +0 -0
  338. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/remo_host_client.py +0 -0
  339. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/rsync.py +0 -0
  340. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/snapshot.py +0 -0
  341. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/ssh.py +0 -0
  342. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/validation.py +0 -0
  343. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/core/version.py +0 -0
  344. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/models/__init__.py +0 -0
  345. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/models/capability.py +0 -0
  346. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/models/discovery.py +0 -0
  347. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/models/host.py +0 -0
  348. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/models/session_target.py +0 -0
  349. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/models/snapshot.py +0 -0
  350. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/providers/__init__.py +0 -0
  351. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/providers/aws.py +0 -0
  352. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/providers/hetzner.py +0 -0
  353. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/providers/incus.py +0 -0
  354. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/providers/proxmox.py +0 -0
  355. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/__init__.py +0 -0
  356. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/__init__.py +0 -0
  357. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/hosts.py +0 -0
  358. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/terminals.py +0 -0
  359. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/discovery.py +0 -0
  360. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/models.py +0 -0
  361. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/ssh_master.py +0 -0
  362. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/state.py +0 -0
  363. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/terminal.py +0 -0
  364. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/terminal_registry.py +0 -0
  365. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/src/remo_cli/web/tokens.py +0 -0
  366. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/__init__.py +0 -0
  367. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/ansible/__init__.py +0 -0
  368. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/ansible/test_remo_host_idempotency.py +0 -0
  369. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/conftest.py +0 -0
  370. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/fixtures.ts +0 -0
  371. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/ghostty-shell-io.spec.ts +0 -0
  372. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/ghostty-tui-compatibility.spec.ts +0 -0
  373. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/keyboard-routing.spec.ts +0 -0
  374. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/mobile.spec.ts +0 -0
  375. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/reconnect.spec.ts +0 -0
  376. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/e2e/workspace-layout.spec.ts +0 -0
  377. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/image/__init__.py +0 -0
  378. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/__init__.py +0 -0
  379. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/_nine_terminal_fixture.py +0 -0
  380. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/orbstack-cloud-init.yml +0 -0
  381. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/orbstack.sh +0 -0
  382. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_nine_terminals.py +0 -0
  383. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_nine_terminals_soak.py +0 -0
  384. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_remo_host_e2e.py +0 -0
  385. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_security_rejections.py +0 -0
  386. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_terminal_attach.py +0 -0
  387. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/integration/test_web_cli_parity.py +0 -0
  388. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/perf/__init__.py +0 -0
  389. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/perf/test_latency.py +0 -0
  390. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/__init__.py +0 -0
  391. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/__init__.py +0 -0
  392. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_aws_snapshot.py +0 -0
  393. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_hetzner_snapshot.py +0 -0
  394. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_incus_snapshot.py +0 -0
  395. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_proxmox_snapshot.py +0 -0
  396. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/test_cp.py +0 -0
  397. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/test_main.py +0 -0
  398. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/cli/test_shell.py +0 -0
  399. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/__init__.py +0 -0
  400. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_ansible_runner.py +0 -0
  401. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_config.py +0 -0
  402. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_known_hosts.py +0 -0
  403. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_output.py +0 -0
  404. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_picker.py +0 -0
  405. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_remo_host_client.py +0 -0
  406. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_rsync.py +0 -0
  407. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_snapshot.py +0 -0
  408. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_ssh.py +0 -0
  409. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_ssh_controlpath.py +0 -0
  410. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_ssh_identity_opts.py +0 -0
  411. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_validation.py +0 -0
  412. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_version.py +0 -0
  413. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_web_adopt_authorize.py +0 -0
  414. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_web_adopt_payload.py +0 -0
  415. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/core/test_web_adopt_trust.py +0 -0
  416. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/models/__init__.py +0 -0
  417. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/models/test_capability.py +0 -0
  418. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/models/test_discovery.py +0 -0
  419. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/models/test_session_target.py +0 -0
  420. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/providers/__init__.py +0 -0
  421. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/providers/test_aws_snapshot.py +0 -0
  422. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/providers/test_hetzner_snapshot.py +0 -0
  423. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/providers/test_incus_snapshot.py +0 -0
  424. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/providers/test_proxmox_devcontainer_runtime.py +0 -0
  425. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/providers/test_proxmox_snapshot.py +0 -0
  426. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/test_ansible_templates.py +0 -0
  427. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/test_host_model.py +0 -0
  428. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/__init__.py +0 -0
  429. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/conftest.py +0 -0
  430. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_backpressure.py +0 -0
  431. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_check.py +0 -0
  432. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_discovery.py +0 -0
  433. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_health_states.py +0 -0
  434. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_hosts_mapping.py +0 -0
  435. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_lazy_import.py +0 -0
  436. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_log_redaction.py +0 -0
  437. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_terminal_resize.py +0 -0
  438. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_terminals_api.py +0 -0
  439. {remo_cli-2.2.0rc2 → remo_cli-2.2.0rc4}/tests/unit/web/test_tokens.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "feature_directory": "specs/012-web-adopt-pairing"
3
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remo-cli
3
- Version: 2.2.0rc2
3
+ Version: 2.2.0rc4
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"]
@@ -139,10 +139,31 @@ services:
139
139
  #
140
140
  # remo web adopt http://<docker-host>:8080
141
141
  #
142
- # which pushes your registry + instance host keys and authorizes the
143
- # service's OWN public key on each instance your personal private key
144
- # never leaves the workstation. Later registry changes are re-pushed
145
- # with `remo web push`.
142
+ # Open http://<docker-host>:8080 in a browser (through your forward-auth
143
+ # proxy, see below), click "Copy pairing code", run the command above, and
144
+ # paste the code when prompted. It pushes your registry + instance host
145
+ # keys and authorizes the service's OWN public key on each instance — your
146
+ # personal private key never leaves the workstation. Later registry changes
147
+ # are re-pushed with `remo web push` (open the dashboard's "Pair CLI to
148
+ # sync" affordance for a fresh code).
149
+ #
150
+ # Setup authorization (012-web-adopt-pairing): there is NO static token. The
151
+ # awaiting-adoption page mints a short-lived, single-use PAIRING CODE; the
152
+ # /api/v1/setup/* surface is dormant (404) unless a pairing session is live.
153
+ # Minting a code is gated by OPERATOR AUTHENTICATION:
154
+ #
155
+ # - Recommended: put a forward-auth reverse proxy (Traefik ForwardAuth /
156
+ # oauth2-proxy / Authelia / a hola app's SSO) in front. It must (a) gate
157
+ # the browser-facing POST /api/v1/pairing/mint so only an authenticated
158
+ # operator can mint, while (b) PASSING THROUGH /api/v1/setup/* unchanged
159
+ # (the CLI cannot do SSO — it authenticates with the pairing code alone).
160
+ # Set REMO_WEB_OPERATOR_AUTH=forward and REMO_WEB_FORWARD_AUTH_HEADER to
161
+ # the trusted identity header your proxy injects (e.g. X-Forwarded-User).
162
+ # The proxy MUST strip that header from inbound client requests.
163
+ #
164
+ # - Loopback/dev only: REMO_WEB_OPERATOR_AUTH=none mints WITHOUT operator
165
+ # auth (a loud, weaker posture surfaced in readiness). Use only when the
166
+ # port is loopback-bound or otherwise network-restricted.
146
167
  #
147
168
  # This service carries `profiles: ["adopted"]` so a plain `docker compose
148
169
  # up` still starts only the bind-mount service above. Start this one with:
@@ -154,25 +175,29 @@ services:
154
175
  # above — the mode is chosen purely by what is mounted, not by the image.
155
176
  image: ghcr.io/get2knowio/remo-web:latest
156
177
  ports:
157
- # Adoption traffic (the token-gated /api/v1/setup/* calls made by
158
- # `remo web adopt`) arrives over this same port. Loopback-only means
159
- # the workstation running the CLI must be the Docker host itself —
160
- # otherwise bind a LAN IP here deliberately, or keep loopback and run
161
- # `remo web adopt --via <docker-host>` to tunnel over SSH.
178
+ # Adoption traffic (the pairing-code-authenticated /api/v1/setup/* calls
179
+ # made by `remo web adopt`) arrives over this same port. Loopback-only
180
+ # means the workstation running the CLI must be the Docker host itself —
181
+ # otherwise bind a LAN IP here deliberately (behind your forward-auth
182
+ # proxy), or keep loopback and run `remo web adopt --via <docker-host>`
183
+ # to tunnel over SSH.
162
184
  - "127.0.0.1:8080:8080"
163
185
  environment:
164
- # REQUIRED for adopted mode: while this is unset/empty the setup API
165
- # does not exist at all (every /api/v1/setup/* request gets a plain
166
- # 404), so adoption is impossible. Generate a strong random value,
167
- # e.g.:
186
+ # Operator-authentication posture for pairing-code minting (see the notes
187
+ # above). Choose ONE:
188
+ #
189
+ # Forward auth (recommended, behind an SSO proxy):
190
+ # - "REMO_WEB_OPERATOR_AUTH=forward"
191
+ # - "REMO_WEB_FORWARD_AUTH_HEADER=X-Forwarded-User"
192
+ # (Enabling forward auth without a header name is a fail-fast error.)
168
193
  #
169
- # openssl rand -hex 24
194
+ # Network-restricted (loopback/dev only — weaker, loudly logged):
195
+ - "REMO_WEB_OPERATOR_AUTH=none"
170
196
  #
171
- # and pass the same value to the CLI via $REMO_API_TOKEN (or the
172
- # interactive prompt) when running `remo web adopt`.
173
- - "REMO_WEB_API_TOKEN=replace-me--generate-with--openssl-rand-hex-24"
197
+ # There is NO REMO_WEB_API_TOKEN anymore a value set for it is ignored.
174
198
  # The REMO_WEB_ALLOWED_HOSTS / REMO_WEB_ALLOWED_ORIGINS notes on the
175
- # service above apply here unchanged.
199
+ # service above apply here unchanged (the mint endpoint needs the page's
200
+ # Origin allowlisted; the default 127.0.0.1:8080 origin covers loopback).
176
201
  volumes:
177
202
  # A writable named state volume at REMO_HOME — the ONLY volume in this
178
203
  # mode (no registry mount, no ~/.ssh mounts; presence of those is what
@@ -182,23 +207,39 @@ services:
182
207
  # a NEW identity on next boot; it revokes nothing on the instances —
183
208
  # remove the old `remo-web@<deployment-id>` authorized_keys line there.
184
209
  - "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.
210
+ # A PLAIN, option-less tmpfs no `rw,mode=1777` pin needed anymore. The
211
+ # image starts as root and its entrypoint re-heals ownership on
212
+ # /run/remo-ssh on EVERY start, so the well-known Docker quirk (an
213
+ # option-less tmpfs is remounted root-owned 0755 by a container *restart*)
214
+ # no longer breaks the startup runtime-dir check on the second boot.
194
215
  tmpfs:
195
- - "/run/remo-ssh:rw,mode=1777"
216
+ - "/run/remo-ssh"
196
217
  read_only: true
197
- user: "1000:1000"
218
+ # NOTE: no `user:` pin here (unlike the bind-mount service above). This
219
+ # service starts as ROOT so the entrypoint can self-heal ownership on the
220
+ # state volume (Docker seeds a fresh named volume root-owned; a bind-mount
221
+ # host dir is root-owned too) before dropping — via gosu — to the
222
+ # unprivileged `remo` user. The app STILL runs as non-root `remo`; only
223
+ # the brief healing pass runs as root. See docker/entrypoint.sh.
198
224
  security_opt:
199
225
  - "no-new-privileges:true"
226
+ # Drop everything, then add back ONLY the capabilities the heal-then-drop
227
+ # entrypoint needs while it is root: CHOWN (fix ownership of the state
228
+ # dir/tmpfs), DAC_OVERRIDE (traverse/write a dir owned by another uid),
229
+ # FOWNER (defensive — chmod/utime on files owned by another uid),
230
+ # SETUID+SETGID (gosu drops to the `remo` uid).
231
+ # The dropped-to `remo` process holds NONE of these effectively — dropping
232
+ # uid clears the permitted/effective sets — so the security posture is
233
+ # unchanged from a plain non-root run; `no-new-privileges` does not block
234
+ # gosu (dropping privileges is always allowed).
200
235
  cap_drop:
201
236
  - "ALL"
237
+ cap_add:
238
+ - "CHOWN"
239
+ - "DAC_OVERRIDE"
240
+ - "FOWNER"
241
+ - "SETUID"
242
+ - "SETGID"
202
243
  healthcheck:
203
244
  # /api/v1/ready answers 200 for BOTH `ready` and `unconfigured` — a
204
245
  # 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
@@ -180,30 +180,47 @@ instance — your own LAN, your own tailnet, or behind a reverse proxy you contr
180
180
 
181
181
  ### Reverse proxies, SSO, and the setup surface
182
182
 
183
- There is exactly one authenticated surface in the service: the **setup API** (`/api/v1/setup/*`) used
184
- by `remo web adopt`/`remo web push`, gated by a bearer token (`REMO_WEB_API_TOKEN` see
185
- [CLI-to-web adoption](#cli-to-web-adoption)). Everything above about the *browser* surface remains
186
- true regardless of deployment mode:
187
-
188
- - **A reverse proxy without SSO does not change the trust model.** Putting the service behind a
189
- TLS-terminating proxy (e.g. an hola app behind Traefik) encrypts the transport, but the dashboard
190
- and terminals still have no login the browser surface still needs proxy-level protection
191
- (network boundary, allowlisted client IPs, or proxy auth) exactly as described above.
192
- - **A future forward-auth deployment needs a bypass rule scoped to `/api/v1/setup/*`.** The
193
- workstation CLI authenticates with the `Authorization: Bearer` token, not with a browser SSO
194
- session, so an SSO/forward-auth layer in front of the service would block adoption unless setup
195
- routes are exempted from it. That bypass is defensible because the application itself enforces the
196
- bearer token on every setup route the surface is never unauthenticated. (Documentation-only
197
- guidance; no proxy/SSO integration ships with the service.)
198
- - **Origin-less requests to the setup surface bypass the Origin allowlist deliberately and
199
- safely.** The Origin allowlist is a browser-CSRF defense, and the setup API is bearer-token-only
200
- with no ambient credentials: a cross-origin browser request cannot attach an `Authorization`
201
- header, and a genuine browser CSRF attempt always carries an `Origin` header — which is still
202
- enforced (a present-but-disallowed `Origin` is rejected). This scoped exemption is what lets the
203
- Origin-less CLI client reach the setup API, including `--via` tunnels whose
204
- `127.0.0.1:<random-port>` origin could never be allowlisted. See the auth section of
205
- [`setup-api.md`](../specs/011-web-adopt/contracts/setup-api.md) and the middleware in
206
- `src/remo_cli/web/app.py`.
183
+ > **Breaking change (012-web-adopt-pairing):** the static `REMO_WEB_API_TOKEN` gate is **removed**. A
184
+ > value set for that variable is now ignored. Setup access is authorized by an **ephemeral pairing
185
+ > code** minted from the awaiting-adoption page, not a long-lived secret.
186
+
187
+ The setup API (`/api/v1/setup/*`) used by `remo web adopt`/`remo web push` is **dormant** — every
188
+ route returns `404`, byte-identical to an unknown route unless a **pairing session** is live. A
189
+ session exists only while an operator is on the awaiting-adoption page (or the dashboard's re-sync
190
+ affordance): opening the page mints a short-lived, single-use pairing code (sliding idle TTL, default
191
+ 15 min), the operator copies it to their workstation and pastes it into the CLI, and the code
192
+ authenticates that one adoption/push. See [CLI-to-web adoption](#cli-to-web-adoption).
193
+
194
+ Two properties make this safe:
195
+
196
+ - **Minting a code is gated by operator authentication.** The browser-facing
197
+ `POST /api/v1/pairing/mint` endpoint only mints for an authenticated operator. v1 implements this
198
+ with **forward auth**: put a proxy (Traefik ForwardAuth / oauth2-proxy / Authelia / a hola app's
199
+ SSO) in front that terminates sign-on and injects a trusted identity header, and set
200
+ `REMO_WEB_OPERATOR_AUTH=forward` + `REMO_WEB_FORWARD_AUTH_HEADER=<that header>` (e.g.
201
+ `X-Forwarded-User`). Enabling forward auth without naming a header is a **fail-fast** startup error.
202
+ A loopback/dev deployment may instead set `REMO_WEB_OPERATOR_AUTH=none` (network-restricted mints
203
+ without operator auth; a loud, weaker posture surfaced in readiness). The check sits behind a
204
+ pluggable provider seam so an in-app OIDC verifier can be added later.
205
+ - **The proxy must split the two paths.** Forward auth applies **only** to `POST /api/v1/pairing/mint`
206
+ — the CLI cannot complete an SSO challenge, so the proxy MUST **pass `/api/v1/setup/*` through**
207
+ unauthenticated at the proxy layer; those routes are authenticated by the pairing code alone. In
208
+ short: gate `/api/v1/pairing/mint` with SSO, pass `/api/v1/setup/*` through.
209
+
210
+ **Forward-auth trust boundary.** The service trusts the identity header only because the deployment
211
+ guarantees the proxy sits in front and **sets/strips** that header — so a client cannot reach the app
212
+ directly and spoof it. This is the standard forward-auth boundary; a deployment that exposes the app
213
+ directly (no proxy) MUST use `REMO_WEB_OPERATOR_AUTH=none` and accept the weaker posture.
214
+
215
+ **Origin-less requests to the setup surface bypass the Origin allowlist — deliberately and safely.**
216
+ The Origin allowlist is a browser-CSRF defense, and the setup API carries no ambient credentials: a
217
+ cross-origin browser request cannot attach an `Authorization` header, and a genuine browser CSRF
218
+ attempt always carries an `Origin` (still enforced). This scoped exemption lets the Origin-less CLI
219
+ reach the setup API, including `--via` tunnels whose `127.0.0.1:<random-port>` origin could never be
220
+ allowlisted. The browser-only `POST /api/v1/pairing/mint` is **not** exempt — it is held to the Origin
221
+ check. See [`setup-api.md`](../specs/012-web-adopt-pairing/contracts/setup-api.md),
222
+ [`pairing-api.md`](../specs/012-web-adopt-pairing/contracts/pairing-api.md), and the middleware in
223
+ `src/remo_cli/web/app.py`.
207
224
 
208
225
  ## Deployment modes: mounts vs adoption
209
226
 
@@ -217,7 +234,7 @@ var that can drift out of sync with reality):
217
234
  | SSH identity | **Your personal private key** bind-mounted read-only | A **service-scoped keypair** the container generates itself on first boot (`web-identity/id_ed25519`, comment `remo-web@<deployment-id>`) |
218
235
  | Instance host keys | Your `~/.ssh/known_hosts` bind-mounted read-only | Verified host keys pushed by the CLI (`web-identity/known_hosts`) |
219
236
  | Volumes | Several read-only bind mounts | **One** writable named volume at `REMO_HOME` (`/home/remo/.config/remo`) — no registry mount, no `~/.ssh` mounts |
220
- | Required env | — | `REMO_WEB_API_TOKEN` (enables the token-gated setup API; without it adoption is impossible) |
237
+ | Required env | — | `REMO_WEB_OPERATOR_AUTH` (`forward` + `REMO_WEB_FORWARD_AUTH_HEADER`, or `none` for loopback/dev) — gates pairing-code minting; without a provider, minting is disabled and adoption is impossible |
221
238
  | Runs where? | Effectively the same machine as your CLI config | Any host — nothing from the workstation is mounted |
222
239
  | Registry updates | Edit/sync locally; the mount hot-reloads | `remo web push` after local changes |
223
240
 
@@ -235,7 +252,7 @@ From these artifacts the service derives one of four states:
235
252
 
236
253
  | State | Derivation | `remo web check` | `GET /api/v1/ready` | Browser |
237
254
  |---|---|---|---|---|
238
- | `unconfigured` | `REMO_HOME` writable, no registry (service keypair may already exist — generated, awaiting first push) | PASS: `unconfigured — awaiting adoption; run 'remo web adopt <service-url>' from a workstation` | **`200`** `{"status": "unconfigured", ...}` — healthy-and-waiting must not fail the compose healthcheck or crash-loop `restart: unless-stopped` | "Awaiting adoption" page: explains the state, shows a copy-pastable `remo web adopt <origin>` command, and flips to the dashboard automatically once adoption completes. No instance data, no terminals, no public-key display. |
255
+ | `unconfigured` | `REMO_HOME` writable, no registry (service keypair may already exist — generated, awaiting first push) | PASS: `unconfigured — awaiting adoption; run 'remo web adopt <service-url>' from a workstation` | **`200`** `{"status": "unconfigured", ...}` — healthy-and-waiting must not fail the compose healthcheck or crash-loop `restart: unless-stopped` | "Awaiting adoption" page: explains the state, shows the `remo web adopt <origin>` command, and a **Copy pairing code** button (the code itself is never displayed). Flips to the dashboard automatically once adoption completes. No instance data, no terminals, no public-key display. |
239
256
  | `adopted` | `REMO_HOME` writable + service keypair + registry present | PASS: `adopted — configured via 'remo web adopt' (service identity in web-identity/)` | `200` `{"status": "ready", ...}` | Normal dashboard |
240
257
  | `mount_configured` | Registry present **and** (`REMO_HOME` not writable — the `:ro` bind mount — or a user SSH identity resolves via `REMO_WEB_SSH_IDENTITY_FILE`/`~/.ssh/id_*`). Explicit mounts are the operator's stated intent, so this wins even if a service keypair also exists. | PASS: `mount_configured — configured via read-only mounts` | `200` `{"status": "ready", ...}` | Normal dashboard |
241
258
  | `broken` | Any required artifact present but unreadable, a half-pair service keypair, a registry on a writable volume with nothing able to authenticate, or a missing runtime prerequisite | FAIL with per-check remediation | `503` `{"status": "not_ready", ...}` with actionable detail — unchanged from today | Offline/error indicator |
@@ -270,9 +287,9 @@ The file defines **both deployment modes as alternative services** — run one o
270
287
 
271
288
  Its differences from `remo-web`: **no bind mounts at all** — a single writable named volume
272
289
  (`remo-web-state:/home/remo/.config/remo`) holds the pushed registry, per-instance host keys, and
273
- the service identity keypair — plus a required `REMO_WEB_API_TOKEN` environment variable (generate
274
- one with `openssl rand -hex 24`). Hardening flags are identical. See
275
- [CLI-to-web adoption](#cli-to-web-adoption) for what happens after `up`.
290
+ the service identity keypair — plus a `REMO_WEB_OPERATOR_AUTH` setting (`forward` behind an SSO
291
+ proxy, or `none` for loopback/dev) that gates pairing-code minting. Hardening flags are identical.
292
+ See [CLI-to-web adoption](#cli-to-web-adoption) for what happens after `up`.
276
293
 
277
294
  ### The published image
278
295
 
@@ -358,7 +375,7 @@ tooling.
358
375
  | Flag | Value | Why |
359
376
  |---|---|---|
360
377
  | `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. |
378
+ | 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
379
  | `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
380
  | `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
381
  | `restart: unless-stopped` | — | Keeps the service available across host reboots/crashes without fighting an operator's deliberate `docker compose stop`. |
@@ -377,39 +394,38 @@ Both commands live in the base CLI (`src/remo_cli/cli/web.py` → `src/remo_cli/
377
394
  stdlib HTTP only) — the `web` extra is **not** required on the workstation:
378
395
 
379
396
  ```text
380
- remo web adopt [URL] [--token TEXT] [--via HOST] [--allow-empty] [--yes] [--save]
381
- remo web push [--allow-empty] [--yes]
397
+ remo web adopt [URL] [--token TEXT] [--via HOST] [--allow-empty] [--yes]
398
+ remo web push [URL] [--token TEXT] [--via HOST] [--allow-empty] [--yes]
382
399
  ```
383
400
 
401
+ `--token` carries the **pairing code** (the option name is kept for
402
+ compatibility). Nothing is saved between runs — each adopt/push obtains a fresh
403
+ code from the page.
404
+
384
405
  ### First-time adoption walkthrough
385
406
 
386
407
  **1. Deploy the container.** Via Compose (`docker compose --profile adopted up -d`, see
387
- [Docker Compose deployment](#docker-compose-deployment)) or as an **hola app** — the hola app
388
- manifest prompts for or generates the admin token at install time and supplies it to the container
389
- as `REMO_WEB_API_TOKEN`; either way the operator's job is the same: a writable state volume plus the
390
- token. Within ~30 seconds the container is up in the `unconfigured` state, has minted its
391
- service-scoped keypair, and the browser shows the "awaiting adoption" page with the exact command to
392
- run.
408
+ [Docker Compose deployment](#docker-compose-deployment)) or as an **hola app** — set
409
+ `REMO_WEB_OPERATOR_AUTH` (`forward` behind the hola app's SSO, plus `REMO_WEB_FORWARD_AUTH_HEADER`;
410
+ or `none` for a loopback/dev deployment) so the page can mint pairing codes. Within ~30 seconds the
411
+ container is up in the `unconfigured` state, has minted its service-scoped keypair, and the browser
412
+ shows the "awaiting adoption" page.
393
413
 
394
- **2. Run `remo web adopt` on the workstation.** Inputs resolve in this order:
414
+ **2. Copy the pairing code and run `remo web adopt`.** On the awaiting-adoption page (reached through
415
+ your SSO proxy), click **Copy pairing code** — the code lands on your clipboard and is never
416
+ displayed. On the workstation, inputs resolve in this order:
395
417
 
396
418
  | Input | Resolution order |
397
419
  |---|---|
398
420
  | Service URL | argument → `REMO_API_URL` env var → interactive prompt |
399
- | API token | `--token` → `REMO_API_TOKEN` env var → interactive prompt (hidden input) |
400
-
401
- `REMO_API_URL`/`REMO_API_TOKEN` are the workstation-side counterparts of the service's
402
- `REMO_WEB_API_TOKEN` — set them (e.g. exported by your shell profile, or handed out by the hola app
403
- install output) and the command runs prompt-free:
421
+ | Pairing code | `--token` → `REMO_API_TOKEN` env var → interactive prompt (hidden input) |
404
422
 
405
423
  ```bash
406
- export REMO_API_URL=http://docker-host.lan:8080
407
- export REMO_API_TOKEN=<the value you set as REMO_WEB_API_TOKEN>
408
- remo web adopt
424
+ remo web adopt http://docker-host.lan:8080 # paste the code at the prompt
409
425
  ```
410
426
 
411
427
  The flow then: checks the service's state (aborting clearly if the target is mount-configured or the
412
- token is rejected), fetches the service's public key and deployment id, and — per direct-access
428
+ code is no longer valid), fetches the service's public key and deployment id, and — per direct-access
413
429
  instance, with a bounded per-instance time budget so one slow instance delays only itself —
414
430
  `ssh-keyscan`s the host, verifies the scanned key against your own trusted `~/.ssh/known_hosts`
415
431
  record (`ssh-keygen -F`, so hashed known_hosts files work; the service itself **never** makes a
@@ -437,52 +453,50 @@ asymmetric-network case (e.g. the instance is only reachable via workstation-spe
437
453
  config such as ProxyJump, or a firewall between the container host and the instance), not an
438
454
  adoption failure.
439
455
 
440
- **5. Saved credentials (explicit consent).** On success the CLI offers to save the service URL and
441
- token to `~/.config/remo/web-service.json` (written atomically, mode `0600`) so that later
442
- `remo web push` runs are zero-argument. Saving requires explicit consent an interactive yes, or
443
- the `--save` flag; `--yes` alone never saves. v1 stores a single default deployment. The file also
444
- records the service's `deployment_id` and a per-instance push cache (see below).
456
+ **5. No saved credentials.** Nothing durable is saved (there is no long-lived secret to save). A later
457
+ `remo web push` obtains a fresh pairing code the same way. The workstation keeps only a **non-secret**
458
+ push cache at `~/.config/remo/web-service.json` (mode `0600`): the service `deployment_id` mapped to
459
+ per-instance host-key fingerprints, used to skip re-keyscanning unchanged instances. No URL and no
460
+ code are ever stored.
445
461
 
446
462
  The command exits `0` when the flow completes — per-instance skips/flags are reported in the
447
- summary, not fatal — and `1` only on hard failure (auth rejection, mount-configured target, empty
448
- registry without `--allow-empty`, tunnel failure, payload rejected). Re-running adopt is idempotent:
449
- same summary, zero changes, still exactly one `remo-web@` line per instance.
450
-
451
- ### The setup API and `REMO_WEB_API_TOKEN`
452
-
453
- The CLI talks to four token-gated endpoints under `/api/v1/setup/*`
454
- ([`setup-api.md`](../specs/011-web-adopt/contracts/setup-api.md)): `GET /status`, `GET /identity`,
455
- `PUT /registry`, `POST /verify`. Every route requires `Authorization: Bearer <REMO_WEB_API_TOKEN>`,
456
- compared in constant time:
457
-
458
- - **Unset/empty token → the setup surface does not exist.** Every `/api/v1/setup/*` request gets a
459
- plain `404`, indistinguishable from an absent feature fail closed, never open. All other
460
- functionality (dashboard, terminals, health) is unaffected. A bind-mount deployment that never
461
- sets the token therefore exposes no setup surface at all.
462
- - **Wrong/missing header`401`** with no further detail; the attempt is logged without the
463
- presented credential. The token value and `Authorization` headers are covered by the service's
464
- existing log redaction (`src/remo_cli/web/logging_config.py`).
465
- - **Rotation = redeploy with a new value.** The token is deploy-time configuration; change it in the
466
- Compose file or hola app settings and restart, then re-run `remo web adopt` with the new token
467
- (saved workstation credentials with the old token fail with a clear re-auth message).
463
+ summary, not fatal — and `1` only on hard failure (dormant setup surface / expired code,
464
+ mount-configured target, empty registry without `--allow-empty`, tunnel failure, payload rejected).
465
+ Re-running adopt (with a fresh code) is idempotent: same summary, zero changes, still exactly one
466
+ `remo-web@` line per instance.
467
+
468
+ ### The setup API and pairing codes
469
+
470
+ The CLI talks to four endpoints under `/api/v1/setup/*`
471
+ ([`setup-api.md`](../specs/012-web-adopt-pairing/contracts/setup-api.md)): `GET /status`,
472
+ `GET /identity`, `PUT /registry`, `POST /verify`. The surface is **dormant** unless a pairing session
473
+ is live; each route requires `Authorization: Bearer <pairing-code>`, compared in constant time:
474
+
475
+ - **No live session the setup surface does not exist.** Every `/api/v1/setup/*` request gets a plain
476
+ `404`, indistinguishable from an absent feature — fail closed. A session is live only while an
477
+ operator is on the awaiting-adoption page (or the dashboard re-sync affordance).
478
+ - **Wrong/missing/expired codethe same dormant `404`** never a distinguishable `401` that would
479
+ reveal a session exists. The attempt is logged without the presented code; codes and `Authorization`
480
+ headers are covered by the service's log redaction (`src/remo_cli/web/logging_config.py`).
481
+ - **The session ends when the flow completes** (on the terminal `POST /verify`), and a code is
482
+ single-use per handoff reopening the page mints a fresh one and invalidates the prior. There is no
483
+ rotation to manage: codes are ephemeral by construction.
468
484
 
469
485
  ### Ongoing pushes: `remo web push`
470
486
 
471
487
  After the initial adoption, local registry changes (a `remo <provider> sync`, a new `create`, a
472
- removal) are re-synced with a zero-argument push:
488
+ removal) are re-synced. Open the dashboard's **Pair CLI to sync** affordance, copy a fresh code, then:
473
489
 
474
490
  ```bash
475
- remo incus sync my-incus-host # e.g. registers a new instance locally
476
- remo web push # re-sync it to the adopted service
491
+ remo incus sync my-incus-host # e.g. registers a new instance locally
492
+ remo web push http://docker-host.lan:8080 # paste the code; re-sync to the service
477
493
  ```
478
494
 
479
- `remo web push` loads the saved credentials, verifies the service still has the same
480
- `deployment_id` (a mismatch means the state volume was reset it aborts with re-adopt guidance),
481
- then runs the adopt flow with **delta behavior**: only new or changed instances are re-keyscanned
482
- and re-authorized; instances unchanged since the last successful push skip that work entirely and
483
- are reported as `unchanged` (their previously verified host keys are reused, since every push
484
- replaces the service's host-keys file wholesale). If no credentials were saved, `push` behaves
485
- exactly like first-time `adopt`, prompting for URL and token.
495
+ `remo web push` resolves URL + code the same way `adopt` does (every run), reads the service's
496
+ `deployment_id`, and runs the adopt flow with **delta behavior**: only new or changed instances are
497
+ re-keyscanned and re-authorized; instances unchanged since the last successful push (per the non-secret
498
+ push cache for that `deployment_id`) skip that work and are reported as `unchanged` (their previously
499
+ verified host keys are reused, since every push replaces the service's host-keys file wholesale).
486
500
 
487
501
  **Mirror semantics — removals propagate, authorization does not.** The push is an exact mirror: the
488
502
  workstation registry is the source of truth, so an instance you removed locally disappears from the
@@ -623,12 +637,13 @@ interactive session (only `remo-host capabilities` is invoked, never `sessions a
623
637
 
624
638
  | Failure | What it means | Fix |
625
639
  |---|---|---|
626
- | `/api/v1/setup/*` returns `404` for everything | `REMO_WEB_API_TOKEN` is unset or empty — the setup surface is disabled entirely (fail closed). | Set `REMO_WEB_API_TOKEN` in the deployment (Compose `environment:` / hola app setting) and restart. |
627
- | `remo web adopt` fails with an auth error (`401`) | The token you supplied doesn't match the service's `REMO_WEB_API_TOKEN`. | Re-check `REMO_API_TOKEN`/`--token` against the deployed value; after a token rotation, re-run adopt with the new value. |
640
+ | `/api/v1/setup/*` returns `404` for everything | No pairing session is live — the surface is dormant (fail closed). | Open the awaiting-adoption page (through your SSO proxy) to mint a code; if the page can't mint, set `REMO_WEB_OPERATOR_AUTH` (`forward` + header, or `none` for loopback). |
641
+ | `remo web adopt`/`push` fails: "pairing code is no longer valid … dormant" | The code expired (idle TTL), was rotated by reopening the page, or was already used. | Reopen the adopt page (or the dashboard's "Pair CLI to sync" affordance) for a fresh code and retry. |
642
+ | Mint page shows "you are not signed in" / `POST /pairing/mint` returns `403` | Forward auth is required but the request reached the service without the trusted identity header. | Ensure the request goes through the SSO proxy that injects `REMO_WEB_FORWARD_AUTH_HEADER`; verify the proxy sets and strips it. |
628
643
  | adopt fails: deployment "configured via read-only mounts" | The target is a bind-mount deployment (`mount_configured`) — its configuration is operator-provided and read-only, so adoption does not apply. | Update the mounted files instead, or deploy the adopted-mode service (writable state volume, no mounts) if you want adoption. |
629
644
  | adopt refuses: empty registry | Your local registry has no instances — pushing would wipe a previously adopted service (a classic wrong-workstation accident). | Register/sync instances first, or pass `--allow-empty` if wiping is intentional. |
630
645
  | `--via` fails naming `REMO_WEB_ALLOWED_HOSTS` | Tunneled requests arrive with a `127.0.0.1` Host header, which the service's Host allowlist rejects. | Add `127.0.0.1` to `REMO_WEB_ALLOWED_HOSTS` (the default includes it). |
631
- | `remo web push` aborts: service identity changed | The saved `deployment_id` no longer matches the service its state volume was reset and it minted a new identity. | Re-run `remo web adopt` (this replaces the stale `remo-web@` entries on your instances with the new key). |
646
+ | After a service state-volume reset, instances keep a stale `remo-web@` line | The reset service minted a new identity; a fresh `remo web adopt` authorizes the new key but does not remove the old line. | Re-run `remo web adopt`, then delete the stale `remo-web@<old-id>` line from each instance's `~/.ssh/authorized_keys`. |
632
647
  | Summary line `security_flagged` (potential MITM warning) | The instance's scanned host key doesn't match your workstation's trusted record; nothing was pushed for it. | Investigate before trusting. If the instance was legitimately rebuilt: `ssh-keygen -R <host>`, reconnect once to re-trust, re-run adopt. |
633
648
  | Verify report: "reachable from workstation but not from the service" | Asymmetric reachability — the CLI reached the instance but the container cannot (DNS, routing, firewall, or workstation-only SSH config like ProxyJump). | Fix the network path from the container host to the instance; the adoption itself succeeded. |
634
649
 
@@ -688,7 +703,10 @@ locally with zero configuration; a container overrides everything via env alone.
688
703
  | `REMO_WEB_SSH_CONTROL_DIR` | `/run/remo-ssh` | Writable directory for SSH ControlMaster sockets (must be tmpfs or otherwise writable under a read-only rootfs). |
689
704
  | `REMO_WEB_FRONTEND_DIST_DIR` | `<repo_root>/frontend/dist` (resolved relative to the installed package) | Directory the built frontend SPA is served from. The Docker image overrides this to `/app/frontend-dist`, matching where the multi-stage build actually copies the built assets. |
690
705
  | `REMO_WEB_SSH_IDENTITY_FILE` | *(unset — falls back to the service keypair under `web-identity/`, then `~/.ssh/id_ed25519`/`id_ecdsa`/`id_rsa`/`id_dsa`)* | Explicit path to the SSH private key used for readiness/`remo web check`'s identity check, when it isn't one of the conventional filenames. |
691
- | `REMO_WEB_API_TOKEN` | *(unset)* | Admin bearer token for the setup API (`/api/v1/setup/*`) used by `remo web adopt`/`remo web push`. **Fail-closed**: while unset or empty, the setup surface is disabled entirely every setup route returns a plain `404` and adoption is impossible; all other functionality is unaffected. Verified with a constant-time comparison and covered by log redaction. Rotation = redeploy with a new value (then re-run `remo web adopt` with it). Generate with e.g. `openssl rand -hex 24`. |
706
+ | `REMO_WEB_OPERATOR_AUTH` | *(unset — minting disabled)* | Operator-authentication posture gating pairing-code minting (`POST /api/v1/pairing/mint`). `forward` requires a trusted proxy-injected identity header (`REMO_WEB_FORWARD_AUTH_HEADER`); `none` mints without operator auth (network-restricted — a loud, weaker posture for loopback/dev). While unset, minting is disabled and adoption is impossible (fail closed). |
707
+ | `REMO_WEB_FORWARD_AUTH_HEADER` | *(unset)* | Name of the trusted identity header your forward-auth proxy injects (e.g. `X-Forwarded-User`, `Remote-User`). **Required** when `REMO_WEB_OPERATOR_AUTH=forward`; enabling forward auth without it is a fail-fast startup error. The proxy MUST set and strip this header. |
708
+ | `REMO_WEB_PAIRING_TTL_S` | `900.0` | Sliding idle TTL (seconds) for a pairing session — it expires this long after the last successful setup call (default 15 min). |
709
+ | `REMO_WEB_API_TOKEN` | *(removed — ignored)* | **Removed in 012.** The static setup-API token is gone; a value set here is ignored (a one-line "now ignored" note is logged at startup). Setup access is authorized by ephemeral pairing codes. |
692
710
 
693
711
  `remo web serve --host`/`--port` are convenience overrides for local runs; every other setting is env-var-only.
694
712
 
@@ -700,4 +718,4 @@ Two variables configure the **CLI** (not the service — hence no `REMO_WEB_` pr
700
718
  | Variable | Used as |
701
719
  |---|---|
702
720
  | `REMO_API_URL` | Service URL fallback when no URL argument is given (before falling back to an interactive prompt). |
703
- | `REMO_API_TOKEN` | API token fallback when `--token` is not given (before falling back to a hidden interactive prompt). Set it to the same value as the service's `REMO_WEB_API_TOKEN`. |
721
+ | `REMO_API_TOKEN` | Pairing-code fallback when `--token` is not given (before falling back to a hidden interactive prompt). Set it to a code freshly minted from the adopt page. |