remo-cli 2.2.0rc3__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 (438) hide show
  1. remo_cli-2.2.0rc4/.specify/feature.json +3 -0
  2. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/PKG-INFO +1 -1
  3. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docker/compose.example.yml +43 -18
  4. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/web-session-interface.md +106 -88
  5. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/api/client.ts +75 -0
  6. remo_cli-2.2.0rc4/frontend/src/components/AwaitingAdoption.tsx +148 -0
  7. remo_cli-2.2.0rc4/frontend/src/components/PairToSync.css +53 -0
  8. remo_cli-2.2.0rc4/frontend/src/components/PairToSync.tsx +110 -0
  9. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/TopBar.tsx +3 -0
  10. remo_cli-2.2.0rc4/frontend/src/lib/clipboard.ts +28 -0
  11. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/pyproject.toml +1 -1
  12. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/contracts/cli-web-adopt.md +63 -0
  13. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/contracts/pairing-api.md +72 -0
  14. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/contracts/setup-api.md +52 -0
  15. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/data-model.md +102 -0
  16. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/plan.md +167 -0
  17. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/quickstart.md +132 -0
  18. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/research.md +287 -0
  19. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/spec.md +435 -0
  20. remo_cli-2.2.0rc4/specs/012-web-adopt-pairing/tasks.md +239 -0
  21. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/web.py +79 -54
  22. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/web_adopt.py +191 -216
  23. remo_cli-2.2.0rc4/src/remo_cli/web/api/pairing.py +81 -0
  24. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/setup.py +49 -31
  25. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/app.py +39 -0
  26. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/check.py +45 -0
  27. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/config.py +24 -4
  28. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/health.py +23 -0
  29. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/logging_config.py +8 -0
  30. remo_cli-2.2.0rc4/src/remo_cli/web/operator_auth.py +109 -0
  31. remo_cli-2.2.0rc4/src/remo_cli/web/pairing.py +142 -0
  32. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/image/test_docker_image.py +50 -26
  33. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_web_adopt_e2e.py +90 -155
  34. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/test_web_adopt_cmd.py +18 -19
  35. remo_cli-2.2.0rc4/tests/unit/core/test_web_adopt_code.py +56 -0
  36. remo_cli-2.2.0rc4/tests/unit/core/test_web_push.py +386 -0
  37. remo_cli-2.2.0rc4/tests/unit/web/_pairing_support.py +53 -0
  38. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_csp.py +6 -4
  39. remo_cli-2.2.0rc4/tests/unit/web/test_mint_gating.py +57 -0
  40. remo_cli-2.2.0rc4/tests/unit/web/test_operator_auth.py +90 -0
  41. remo_cli-2.2.0rc4/tests/unit/web/test_pairing.py +116 -0
  42. remo_cli-2.2.0rc4/tests/unit/web/test_pairing_origin.py +43 -0
  43. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_setup_api.py +34 -12
  44. remo_cli-2.2.0rc4/tests/unit/web/test_setup_auth.py +273 -0
  45. remo_cli-2.2.0rc4/tests/unit/web/test_setup_dormancy.py +110 -0
  46. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_state.py +11 -5
  47. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/uv.lock +1 -1
  48. remo_cli-2.2.0rc3/.specify/feature.json +0 -3
  49. remo_cli-2.2.0rc3/frontend/src/components/AwaitingAdoption.tsx +0 -109
  50. remo_cli-2.2.0rc3/tests/unit/core/test_web_push.py +0 -630
  51. remo_cli-2.2.0rc3/tests/unit/web/test_setup_auth.py +0 -378
  52. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-analyze/SKILL.md +0 -0
  53. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-checklist/SKILL.md +0 -0
  54. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-clarify/SKILL.md +0 -0
  55. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-constitution/SKILL.md +0 -0
  56. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-converge/SKILL.md +0 -0
  57. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
  58. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
  59. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
  60. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
  61. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
  62. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-implement/SKILL.md +0 -0
  63. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-plan/SKILL.md +0 -0
  64. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-specify/SKILL.md +0 -0
  65. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-tasks/SKILL.md +0 -0
  66. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
  67. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.devcontainer/devcontainer.json +0 -0
  68. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.dockerignore +0 -0
  69. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/dependabot.yml +0 -0
  70. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/workflows/ci.yml +0 -0
  71. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/workflows/provision.yml +0 -0
  72. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/workflows/release.yml +0 -0
  73. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/workflows/smoke-test-cleanup.yml +0 -0
  74. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/workflows/smoke-test.yml +0 -0
  75. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.github/workflows/teardown.yml +0 -0
  76. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.gitignore +0 -0
  77. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
  78. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
  79. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.maverick/checkpoints/feature/validation_complete.json +0 -0
  80. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/.registry +0 -0
  81. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/README.md +0 -0
  82. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
  83. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
  84. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
  85. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
  86. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
  87. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/config-template.yml +0 -0
  88. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/extension.yml +0 -0
  89. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/git-config.yml +0 -0
  90. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
  91. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
  92. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
  93. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
  94. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
  95. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
  96. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
  97. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
  98. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/extensions.yml +0 -0
  99. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/init-options.json +0 -0
  100. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/integration.json +0 -0
  101. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/integrations/claude.manifest.json +0 -0
  102. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/integrations/speckit.manifest.json +0 -0
  103. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/memory/constitution.md +0 -0
  104. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  105. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/scripts/bash/common.sh +0 -0
  106. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/scripts/bash/create-new-feature.sh +0 -0
  107. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/scripts/bash/setup-plan.sh +0 -0
  108. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/scripts/bash/setup-tasks.sh +0 -0
  109. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/scripts/bash/update-agent-context.sh +0 -0
  110. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/templates/agent-file-template.md +0 -0
  111. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/templates/checklist-template.md +0 -0
  112. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/templates/constitution-template.md +0 -0
  113. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/templates/plan-template.md +0 -0
  114. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/templates/spec-template.md +0 -0
  115. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/templates/tasks-template.md +0 -0
  116. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/workflows/speckit/workflow.yml +0 -0
  117. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/.specify/workflows/workflow-registry.json +0 -0
  118. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/AGENTS.md +0 -0
  119. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/CLAUDE.md +0 -0
  120. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/CONTRIBUTING.md +0 -0
  121. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/Dockerfile +0 -0
  122. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/LICENSE +0 -0
  123. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/README.md +0 -0
  124. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/.ansible-lint +0 -0
  125. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/README.md +0 -0
  126. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/ansible.cfg +0 -0
  127. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/aws_configure.yml +0 -0
  128. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/aws_resize.yml +0 -0
  129. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/aws_site.yml +0 -0
  130. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/aws_teardown.yml +0 -0
  131. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/group_vars/all.yml +0 -0
  132. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/group_vars/incus_containers.yml +0 -0
  133. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/group_vars/proxmox_containers.yml +0 -0
  134. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/hetzner_configure.yml +0 -0
  135. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/hetzner_provision.yml +0 -0
  136. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/hetzner_resize.yml +0 -0
  137. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/hetzner_site.yml +0 -0
  138. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/hetzner_teardown.yml +0 -0
  139. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/incus_bootstrap.yml +0 -0
  140. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/incus_configure.yml +0 -0
  141. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/incus_provision.yml +0 -0
  142. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/incus_resize.yml +0 -0
  143. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/incus_site.yml +0 -0
  144. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/incus_teardown.yml +0 -0
  145. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/inventory/hosts.yml +0 -0
  146. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/inventory/incus_containers.yml +0 -0
  147. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/proxmox_bootstrap.yml +0 -0
  148. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/proxmox_configure.yml +0 -0
  149. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/proxmox_provision.yml +0 -0
  150. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/proxmox_resize.yml +0 -0
  151. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/proxmox_site.yml +0 -0
  152. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/proxmox_teardown.yml +0 -0
  153. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/requirements.yml +0 -0
  154. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/defaults/main.yml +0 -0
  155. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
  156. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
  157. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/main.yml +0 -0
  158. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
  159. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
  160. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
  161. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/deacon/defaults/main.yml +0 -0
  162. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/deacon/tasks/main.yml +0 -0
  163. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/devcontainers/tasks/main.yml +0 -0
  164. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/docker/handlers/main.yml +0 -0
  165. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/docker/tasks/main.yml +0 -0
  166. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/fzf/tasks/main.yml +0 -0
  167. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/github_cli/tasks/main.yml +0 -0
  168. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
  169. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
  170. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
  171. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
  172. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
  173. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/defaults/main.yml +0 -0
  174. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/handlers/main.yml +0 -0
  175. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/meta/main.yml +0 -0
  176. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/tasks/main.yml +0 -0
  177. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
  178. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
  179. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
  180. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
  181. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/nodejs/defaults/main.yml +0 -0
  182. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/nodejs/tasks/main.yml +0 -0
  183. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
  184. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
  185. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
  186. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
  187. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
  188. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/meta/main.yml +0 -0
  189. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
  190. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
  191. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
  192. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
  193. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
  194. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/defaults/main.yml +0 -0
  195. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/tasks/main.yml +0 -0
  196. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
  197. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/project-launch.sh.j2 +0 -0
  198. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
  199. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/user_setup/templates/remo-host.sh.j2 +0 -0
  200. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/zellij/defaults/main.yml +0 -0
  201. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/zellij/tasks/main.yml +0 -0
  202. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
  203. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
  204. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/ansible/tasks/configure_dev_tools.yml +0 -0
  205. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docker/Dockerfile +0 -0
  206. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docker/entrypoint.sh +0 -0
  207. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/aws.md +0 -0
  208. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/hetzner.md +0 -0
  209. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/incus.md +0 -0
  210. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/install.sh +0 -0
  211. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/proxmox.md +0 -0
  212. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/remo-web-speckit-input.md +0 -0
  213. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/docs/remo-web.html +0 -0
  214. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/index.html +0 -0
  215. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/package-lock.json +0 -0
  216. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/package.json +0 -0
  217. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/public/README.md +0 -0
  218. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/public/dist__ghostty-vt.wasm +0 -0
  219. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/public/ghostty-vt.wasm +0 -0
  220. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/scripts/copy-ghostty-wasm.mjs +0 -0
  221. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/AppRoot.tsx +0 -0
  222. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/AppShell.css +0 -0
  223. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/AppShell.tsx +0 -0
  224. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/AwaitingAdoption.css +0 -0
  225. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/OfflineOverlay.css +0 -0
  226. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/OfflineOverlay.tsx +0 -0
  227. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/SessionRail.css +0 -0
  228. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/SessionRail.tsx +0 -0
  229. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/SettingsPage.css +0 -0
  230. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/SettingsPage.tsx +0 -0
  231. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/ShortcutsModal.css +0 -0
  232. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/ShortcutsModal.tsx +0 -0
  233. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/TerminalCard.css +0 -0
  234. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/TerminalCard.tsx +0 -0
  235. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/TopBar.css +0 -0
  236. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/WorkspacePane.css +0 -0
  237. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/WorkspacePane.tsx +0 -0
  238. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/providerMeta.ts +0 -0
  239. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/components/railModel.ts +0 -0
  240. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/main.tsx +0 -0
  241. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/state/discovery.ts +0 -0
  242. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/state/fonts.ts +0 -0
  243. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/state/health.ts +0 -0
  244. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/state/settings.ts +0 -0
  245. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/state/useConsoleKeyboard.ts +0 -0
  246. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/state/workspace.ts +0 -0
  247. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/terminal/.gitkeep +0 -0
  248. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/terminal/GhosttyRenderer.ts +0 -0
  249. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/terminal/RendererAdapter.ts +0 -0
  250. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/terminal/TerminalConnection.ts +0 -0
  251. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/terminal/XtermRenderer.ts +0 -0
  252. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/terminal/defaultRenderer.ts +0 -0
  253. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/theme/fonts.ts +0 -0
  254. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/src/theme/tokens.css +0 -0
  255. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/tsconfig.json +0 -0
  256. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/frontend/vite.config.ts +0 -0
  257. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/install.sh +0 -0
  258. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/maverick.yaml +0 -0
  259. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/playwright.config.ts +0 -0
  260. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/project-menu.sh +0 -0
  261. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/remo +0 -0
  262. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/run.sh +0 -0
  263. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/scripts/setup-aws-oidc.sh +0 -0
  264. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
  265. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
  266. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
  267. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
  268. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/data-model.md +0 -0
  269. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/plan.md +0 -0
  270. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
  271. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/research.md +0 -0
  272. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/spec.md +0 -0
  273. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/001-bootstrap-incus-host/tasks.md +0 -0
  274. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/checklists/requirements.md +0 -0
  275. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
  276. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
  277. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/data-model.md +0 -0
  278. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/plan.md +0 -0
  279. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/quickstart.md +0 -0
  280. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/research.md +0 -0
  281. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/spec.md +0 -0
  282. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/002-incus-container-support/tasks.md +0 -0
  283. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
  284. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/data-model.md +0 -0
  285. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/plan.md +0 -0
  286. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/quickstart.md +0 -0
  287. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/research.md +0 -0
  288. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/spec.md +0 -0
  289. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/003-python-cli-rewrite/tasks.md +0 -0
  290. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/004-proxmox-container-support/spec.md +0 -0
  291. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/checklists/requirements.md +0 -0
  292. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/contracts/cli-surface.md +0 -0
  293. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/data-model.md +0 -0
  294. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/plan.md +0 -0
  295. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/quickstart.md +0 -0
  296. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/research.md +0 -0
  297. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/spec.md +0 -0
  298. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/005-provider-snapshots/tasks.md +0 -0
  299. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/checklists/requirements.md +0 -0
  300. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/contracts/remo-host-protocol.md +0 -0
  301. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/contracts/rest-api.md +0 -0
  302. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/contracts/terminal-websocket.md +0 -0
  303. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/data-model.md +0 -0
  304. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/plan.md +0 -0
  305. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/quickstart.md +0 -0
  306. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/research.md +0 -0
  307. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/spec.md +0 -0
  308. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/010-web-session-interface/tasks.md +0 -0
  309. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/checklists/requirements.md +0 -0
  310. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/contracts/cli-web-adopt.md +0 -0
  311. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/contracts/setup-api.md +0 -0
  312. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/data-model.md +0 -0
  313. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/plan.md +0 -0
  314. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/quickstart-results.md +0 -0
  315. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/quickstart.md +0 -0
  316. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/research.md +0 -0
  317. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/spec.md +0 -0
  318. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/specs/011-web-adopt/tasks.md +0 -0
  319. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/__init__.py +0 -0
  320. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/__main__.py +0 -0
  321. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/__init__.py +0 -0
  322. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/cp.py +0 -0
  323. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/main.py +0 -0
  324. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/__init__.py +0 -0
  325. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/aws.py +0 -0
  326. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/hetzner.py +0 -0
  327. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/incus.py +0 -0
  328. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/providers/proxmox.py +0 -0
  329. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/cli/shell.py +0 -0
  330. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/__init__.py +0 -0
  331. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/ansible_runner.py +0 -0
  332. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/completion.py +0 -0
  333. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/config.py +0 -0
  334. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/known_hosts.py +0 -0
  335. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/output.py +0 -0
  336. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/picker.py +0 -0
  337. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/remo_host_client.py +0 -0
  338. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/rsync.py +0 -0
  339. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/snapshot.py +0 -0
  340. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/ssh.py +0 -0
  341. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/validation.py +0 -0
  342. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/core/version.py +0 -0
  343. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/models/__init__.py +0 -0
  344. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/models/capability.py +0 -0
  345. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/models/discovery.py +0 -0
  346. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/models/host.py +0 -0
  347. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/models/session_target.py +0 -0
  348. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/models/snapshot.py +0 -0
  349. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/providers/__init__.py +0 -0
  350. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/providers/aws.py +0 -0
  351. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/providers/hetzner.py +0 -0
  352. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/providers/incus.py +0 -0
  353. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/providers/proxmox.py +0 -0
  354. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/__init__.py +0 -0
  355. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/__init__.py +0 -0
  356. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/hosts.py +0 -0
  357. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/api/terminals.py +0 -0
  358. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/discovery.py +0 -0
  359. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/models.py +0 -0
  360. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/ssh_master.py +0 -0
  361. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/state.py +0 -0
  362. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/terminal.py +0 -0
  363. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/terminal_registry.py +0 -0
  364. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/src/remo_cli/web/tokens.py +0 -0
  365. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/__init__.py +0 -0
  366. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/ansible/__init__.py +0 -0
  367. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/ansible/test_remo_host_idempotency.py +0 -0
  368. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/conftest.py +0 -0
  369. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/fixtures.ts +0 -0
  370. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/ghostty-shell-io.spec.ts +0 -0
  371. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/ghostty-tui-compatibility.spec.ts +0 -0
  372. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/keyboard-routing.spec.ts +0 -0
  373. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/mobile.spec.ts +0 -0
  374. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/reconnect.spec.ts +0 -0
  375. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/e2e/workspace-layout.spec.ts +0 -0
  376. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/image/__init__.py +0 -0
  377. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/__init__.py +0 -0
  378. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/_nine_terminal_fixture.py +0 -0
  379. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/orbstack-cloud-init.yml +0 -0
  380. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/orbstack.sh +0 -0
  381. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_nine_terminals.py +0 -0
  382. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_nine_terminals_soak.py +0 -0
  383. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_remo_host_e2e.py +0 -0
  384. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_security_rejections.py +0 -0
  385. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_terminal_attach.py +0 -0
  386. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/integration/test_web_cli_parity.py +0 -0
  387. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/perf/__init__.py +0 -0
  388. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/perf/test_latency.py +0 -0
  389. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/__init__.py +0 -0
  390. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/__init__.py +0 -0
  391. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_aws_snapshot.py +0 -0
  392. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_hetzner_snapshot.py +0 -0
  393. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_incus_snapshot.py +0 -0
  394. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/providers/test_proxmox_snapshot.py +0 -0
  395. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/test_cp.py +0 -0
  396. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/test_main.py +0 -0
  397. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/cli/test_shell.py +0 -0
  398. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/__init__.py +0 -0
  399. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_ansible_runner.py +0 -0
  400. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_config.py +0 -0
  401. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_known_hosts.py +0 -0
  402. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_output.py +0 -0
  403. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_picker.py +0 -0
  404. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_remo_host_client.py +0 -0
  405. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_rsync.py +0 -0
  406. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_snapshot.py +0 -0
  407. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_ssh.py +0 -0
  408. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_ssh_controlpath.py +0 -0
  409. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_ssh_identity_opts.py +0 -0
  410. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_validation.py +0 -0
  411. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_version.py +0 -0
  412. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_web_adopt_authorize.py +0 -0
  413. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_web_adopt_payload.py +0 -0
  414. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/core/test_web_adopt_trust.py +0 -0
  415. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/models/__init__.py +0 -0
  416. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/models/test_capability.py +0 -0
  417. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/models/test_discovery.py +0 -0
  418. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/models/test_session_target.py +0 -0
  419. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/providers/__init__.py +0 -0
  420. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/providers/test_aws_snapshot.py +0 -0
  421. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/providers/test_hetzner_snapshot.py +0 -0
  422. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/providers/test_incus_snapshot.py +0 -0
  423. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/providers/test_proxmox_devcontainer_runtime.py +0 -0
  424. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/providers/test_proxmox_snapshot.py +0 -0
  425. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/test_ansible_templates.py +0 -0
  426. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/test_host_model.py +0 -0
  427. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/__init__.py +0 -0
  428. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/conftest.py +0 -0
  429. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_backpressure.py +0 -0
  430. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_check.py +0 -0
  431. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_discovery.py +0 -0
  432. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_health_states.py +0 -0
  433. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_hosts_mapping.py +0 -0
  434. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_lazy_import.py +0 -0
  435. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_log_redaction.py +0 -0
  436. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_terminal_resize.py +0 -0
  437. {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc4}/tests/unit/web/test_terminals_api.py +0 -0
  438. {remo_cli-2.2.0rc3 → 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.0rc3
3
+ Version: 2.2.0rc4
4
4
  Summary: Remote development environment CLI
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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
@@ -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
 
@@ -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. |
@@ -220,6 +220,81 @@ export async function getReady(): Promise<ReadinessResponse> {
220
220
  };
221
221
  }
222
222
 
223
+ // ---- Pairing (012-web-adopt-pairing) ----
224
+ //
225
+ // The awaiting-adoption page (and the dashboard re-sync affordance) mints an
226
+ // ephemeral pairing code on open, which the operator copies to the clipboard
227
+ // and pastes into `remo web adopt` / `remo web push`. The code is returned only
228
+ // by mintPairingCode() at runtime — never embedded in the bundle (FR-016) — and
229
+ // the caller MUST hold it out of the DOM (copy-only, never displayed).
230
+
231
+ export interface MintPairingResponse {
232
+ code: string;
233
+ expires_in: number;
234
+ }
235
+
236
+ /**
237
+ * `POST /api/v1/pairing/mint` — mint a fresh code (rotation-on-open, FR-003).
238
+ * `origin` distinguishes the adopt page from the dashboard re-sync affordance.
239
+ * A `403` means operator authentication is required/not configured (the page is
240
+ * reached through a forward-auth proxy in the gated posture) — surfaced via
241
+ * ApiError so the page can show guidance rather than a code.
242
+ */
243
+ export async function mintPairingCode(
244
+ origin: "adopt" | "resync" = "adopt",
245
+ ): Promise<MintPairingResponse> {
246
+ let response: Response;
247
+ try {
248
+ response = await fetch(`/api/v1/pairing/mint?origin=${encodeURIComponent(origin)}`, {
249
+ method: "POST",
250
+ });
251
+ } catch (cause) {
252
+ throw new ApiError({
253
+ code: "network_error",
254
+ message: cause instanceof Error ? cause.message : "Network request failed",
255
+ retryable: true,
256
+ remediation: "Check that the Remo web service is reachable.",
257
+ });
258
+ }
259
+ if (response.status === 403) {
260
+ // Operator authentication required / not configured — surface a distinct
261
+ // code so the adopt page can prompt to sign in rather than showing a code.
262
+ throw new ApiError({
263
+ code: "forbidden",
264
+ message: "Operator authentication is required to mint a pairing code.",
265
+ retryable: false,
266
+ remediation: "Sign in through your access proxy, then reload this page.",
267
+ });
268
+ }
269
+ if (!response.ok) {
270
+ throw new ApiError({
271
+ code: "unknown",
272
+ message: `Mint failed with status ${response.status}`,
273
+ retryable: true,
274
+ remediation: "Reload this page to try again.",
275
+ });
276
+ }
277
+ return (await response.json()) as MintPairingResponse;
278
+ }
279
+
280
+ /**
281
+ * `POST /api/v1/pairing/end` — best-effort session end (page-hide, FR-004).
282
+ * Uses `navigator.sendBeacon` when available so it survives page unload; the
283
+ * server treats it as idempotent and the idle TTL is the authoritative backstop.
284
+ */
285
+ export function endPairing(): void {
286
+ const path = "/api/v1/pairing/end";
287
+ try {
288
+ if (navigator.sendBeacon?.(path)) {
289
+ return;
290
+ }
291
+ } catch {
292
+ // fall through to fetch
293
+ }
294
+ // keepalive lets the request outlive the page during unload.
295
+ void fetch(path, { method: "POST", keepalive: true }).catch(() => undefined);
296
+ }
297
+
223
298
  // ---- Terminals (T041, US2) ----
224
299
  //
225
300
  // Per contracts/rest-api.md and contracts/terminal-websocket.md. The token