remo-cli 2.2.0rc3__tar.gz → 2.2.0rc5__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.
- remo_cli-2.2.0rc5/.specify/feature.json +3 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/PKG-INFO +1 -1
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docker/compose.example.yml +43 -18
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/web-session-interface.md +106 -88
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/api/client.ts +160 -0
- remo_cli-2.2.0rc5/frontend/src/components/AwaitingAdoption.tsx +148 -0
- remo_cli-2.2.0rc5/frontend/src/components/PairToSync.css +53 -0
- remo_cli-2.2.0rc5/frontend/src/components/PairToSync.tsx +110 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/TopBar.tsx +3 -0
- remo_cli-2.2.0rc5/frontend/src/lib/clipboard.ts +28 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/pyproject.toml +1 -1
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/contracts/cli-web-adopt.md +63 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/contracts/pairing-api.md +72 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/contracts/setup-api.md +52 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/data-model.md +102 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/plan.md +167 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/quickstart.md +132 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/research.md +287 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/spec.md +435 -0
- remo_cli-2.2.0rc5/specs/012-web-adopt-pairing/tasks.md +239 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/web.py +79 -54
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/web_adopt.py +191 -216
- remo_cli-2.2.0rc5/src/remo_cli/web/api/pairing.py +81 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/api/setup.py +49 -31
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/app.py +39 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/check.py +45 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/config.py +24 -4
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/health.py +23 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/logging_config.py +8 -0
- remo_cli-2.2.0rc5/src/remo_cli/web/operator_auth.py +109 -0
- remo_cli-2.2.0rc5/src/remo_cli/web/pairing.py +142 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/image/test_docker_image.py +50 -26
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_web_adopt_e2e.py +90 -155
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/test_web_adopt_cmd.py +18 -19
- remo_cli-2.2.0rc5/tests/unit/core/test_web_adopt_code.py +56 -0
- remo_cli-2.2.0rc5/tests/unit/core/test_web_push.py +386 -0
- remo_cli-2.2.0rc5/tests/unit/web/_pairing_support.py +53 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_csp.py +6 -4
- remo_cli-2.2.0rc5/tests/unit/web/test_mint_gating.py +57 -0
- remo_cli-2.2.0rc5/tests/unit/web/test_operator_auth.py +90 -0
- remo_cli-2.2.0rc5/tests/unit/web/test_pairing.py +116 -0
- remo_cli-2.2.0rc5/tests/unit/web/test_pairing_origin.py +43 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_setup_api.py +34 -12
- remo_cli-2.2.0rc5/tests/unit/web/test_setup_auth.py +273 -0
- remo_cli-2.2.0rc5/tests/unit/web/test_setup_dormancy.py +110 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_state.py +11 -5
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/uv.lock +1 -1
- remo_cli-2.2.0rc3/.specify/feature.json +0 -3
- remo_cli-2.2.0rc3/frontend/src/components/AwaitingAdoption.tsx +0 -109
- remo_cli-2.2.0rc3/tests/unit/core/test_web_push.py +0 -630
- remo_cli-2.2.0rc3/tests/unit/web/test_setup_auth.py +0 -378
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-analyze/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-checklist/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-clarify/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-constitution/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-converge/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-implement/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-plan/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-specify/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-tasks/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.devcontainer/devcontainer.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.dockerignore +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/dependabot.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/workflows/ci.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/workflows/provision.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/workflows/release.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/workflows/smoke-test-cleanup.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/workflows/smoke-test.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.github/workflows/teardown.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.gitignore +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.maverick/checkpoints/feature/validation_complete.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/.registry +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/README.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/config-template.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/extension.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/git-config.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/extensions.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/init-options.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/integration.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/integrations/claude.manifest.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/integrations/speckit.manifest.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/memory/constitution.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/scripts/bash/common.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/scripts/bash/setup-plan.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/scripts/bash/setup-tasks.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/scripts/bash/update-agent-context.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/templates/agent-file-template.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/templates/checklist-template.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/templates/constitution-template.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/templates/plan-template.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/templates/spec-template.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/templates/tasks-template.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/workflows/speckit/workflow.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/.specify/workflows/workflow-registry.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/AGENTS.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/CLAUDE.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/CONTRIBUTING.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/Dockerfile +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/LICENSE +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/README.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/.ansible-lint +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/README.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/ansible.cfg +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/aws_configure.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/aws_resize.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/aws_site.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/aws_teardown.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/group_vars/all.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/group_vars/incus_containers.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/group_vars/proxmox_containers.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/hetzner_configure.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/hetzner_provision.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/hetzner_resize.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/hetzner_site.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/hetzner_teardown.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/incus_bootstrap.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/incus_configure.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/incus_provision.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/incus_resize.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/incus_site.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/incus_teardown.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/inventory/hosts.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/inventory/incus_containers.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/proxmox_bootstrap.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/proxmox_configure.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/proxmox_provision.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/proxmox_resize.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/proxmox_site.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/proxmox_teardown.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/requirements.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/deacon/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/deacon/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/devcontainers/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/docker/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/docker/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/fzf/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/github_cli/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container/meta/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/nodejs/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/nodejs/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container/meta/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/user_setup/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/user_setup/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/user_setup/templates/project-launch.sh.j2 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/user_setup/templates/remo-host.sh.j2 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/zellij/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/zellij/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/ansible/tasks/configure_dev_tools.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docker/Dockerfile +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docker/entrypoint.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/aws.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/hetzner.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/incus.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/install.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/proxmox.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/remo-web-speckit-input.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/docs/remo-web.html +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/index.html +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/package-lock.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/package.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/public/README.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/public/dist__ghostty-vt.wasm +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/public/ghostty-vt.wasm +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/scripts/copy-ghostty-wasm.mjs +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/AppRoot.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/AppShell.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/AppShell.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/AwaitingAdoption.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/OfflineOverlay.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/OfflineOverlay.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/SessionRail.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/SessionRail.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/SettingsPage.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/SettingsPage.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/ShortcutsModal.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/ShortcutsModal.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/TerminalCard.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/TerminalCard.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/TopBar.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/WorkspacePane.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/WorkspacePane.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/providerMeta.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/components/railModel.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/main.tsx +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/state/discovery.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/state/fonts.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/state/health.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/state/settings.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/state/useConsoleKeyboard.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/state/workspace.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/terminal/.gitkeep +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/terminal/GhosttyRenderer.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/terminal/RendererAdapter.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/terminal/TerminalConnection.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/terminal/XtermRenderer.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/terminal/defaultRenderer.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/theme/fonts.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/src/theme/tokens.css +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/tsconfig.json +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/frontend/vite.config.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/install.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/maverick.yaml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/playwright.config.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/project-menu.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/remo +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/run.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/scripts/setup-aws-oidc.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/data-model.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/plan.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/research.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/001-bootstrap-incus-host/tasks.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/data-model.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/plan.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/quickstart.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/research.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/002-incus-container-support/tasks.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/data-model.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/plan.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/quickstart.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/research.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/003-python-cli-rewrite/tasks.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/004-proxmox-container-support/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/contracts/cli-surface.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/data-model.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/plan.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/quickstart.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/research.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/005-provider-snapshots/tasks.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/contracts/remo-host-protocol.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/contracts/rest-api.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/contracts/terminal-websocket.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/data-model.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/plan.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/quickstart.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/research.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/010-web-session-interface/tasks.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/contracts/cli-web-adopt.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/contracts/setup-api.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/data-model.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/plan.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/quickstart-results.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/quickstart.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/research.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/spec.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/specs/011-web-adopt/tasks.md +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/__main__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/cp.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/main.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/providers/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/providers/aws.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/providers/hetzner.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/providers/incus.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/providers/proxmox.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/cli/shell.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/ansible_runner.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/completion.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/config.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/known_hosts.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/output.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/picker.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/remo_host_client.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/rsync.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/ssh.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/validation.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/core/version.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/models/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/models/capability.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/models/discovery.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/models/host.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/models/session_target.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/models/snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/providers/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/providers/aws.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/providers/hetzner.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/providers/incus.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/providers/proxmox.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/api/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/api/hosts.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/api/terminals.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/discovery.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/models.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/ssh_master.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/state.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/terminal.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/terminal_registry.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/src/remo_cli/web/tokens.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/ansible/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/ansible/test_remo_host_idempotency.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/conftest.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/fixtures.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/ghostty-shell-io.spec.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/ghostty-tui-compatibility.spec.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/keyboard-routing.spec.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/mobile.spec.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/reconnect.spec.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/e2e/workspace-layout.spec.ts +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/image/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/_nine_terminal_fixture.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/orbstack-cloud-init.yml +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/orbstack.sh +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_nine_terminals.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_nine_terminals_soak.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_remo_host_e2e.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_security_rejections.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_terminal_attach.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/integration/test_web_cli_parity.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/perf/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/perf/test_latency.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/providers/test_aws_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/providers/test_hetzner_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/providers/test_incus_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/providers/test_proxmox_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/test_cp.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/test_main.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/cli/test_shell.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_ansible_runner.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_config.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_known_hosts.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_output.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_picker.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_remo_host_client.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_rsync.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_ssh.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_ssh_controlpath.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_ssh_identity_opts.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_validation.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_version.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_web_adopt_authorize.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_web_adopt_payload.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/core/test_web_adopt_trust.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/models/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/models/test_capability.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/models/test_discovery.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/models/test_session_target.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/providers/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/providers/test_aws_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/providers/test_hetzner_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/providers/test_incus_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/providers/test_proxmox_devcontainer_runtime.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/providers/test_proxmox_snapshot.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/test_ansible_templates.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/test_host_model.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/__init__.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/conftest.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_backpressure.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_check.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_discovery.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_health_states.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_hosts_mapping.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_lazy_import.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_log_redaction.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_terminal_resize.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_terminals_api.py +0 -0
- {remo_cli-2.2.0rc3 → remo_cli-2.2.0rc5}/tests/unit/web/test_tokens.py +0 -0
|
@@ -139,10 +139,31 @@ services:
|
|
|
139
139
|
#
|
|
140
140
|
# remo web adopt http://<docker-host>:8080
|
|
141
141
|
#
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
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
|
|
158
|
-
# `remo web adopt`) arrives over this same port. Loopback-only
|
|
159
|
-
# the workstation running the CLI must be the Docker host itself —
|
|
160
|
-
# otherwise bind a LAN IP here deliberately
|
|
161
|
-
# `remo web adopt --via <docker-host>`
|
|
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
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
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
|
-
#
|
|
194
|
+
# Network-restricted (loopback/dev only — weaker, loudly logged):
|
|
195
|
+
- "REMO_WEB_OPERATOR_AUTH=none"
|
|
170
196
|
#
|
|
171
|
-
#
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 | — | `
|
|
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
|
|
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
|
|
274
|
-
|
|
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]
|
|
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** —
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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.
|
|
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
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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 (
|
|
448
|
-
registry without `--allow-empty`, tunnel failure, payload rejected).
|
|
449
|
-
same summary, zero changes, still exactly one
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
- **Wrong/missing
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
- **
|
|
466
|
-
|
|
467
|
-
|
|
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 code → the 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
|
|
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
|
|
476
|
-
remo web push
|
|
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`
|
|
480
|
-
`deployment_id
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 |
|
|
627
|
-
| `remo web adopt` fails
|
|
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
|
-
|
|
|
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
|
-
| `
|
|
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` |
|
|
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. |
|
|
@@ -101,11 +101,82 @@ interface ErrorEnvelope {
|
|
|
101
101
|
error: TypedError;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// ---- Forward-auth (SSO proxy) re-authentication ----
|
|
105
|
+
//
|
|
106
|
+
// When remo-web is deployed behind a whole-app forward-auth proxy (Traefik
|
|
107
|
+
// ForwardAuth + an OIDC IdP such as Authentik — e.g. a Hola app), an
|
|
108
|
+
// unauthenticated or expired-session request is answered with a 302 to the
|
|
109
|
+
// cross-origin IdP (`https://auth.example.com/application/o/authorize/...`). A
|
|
110
|
+
// same-origin `fetch()` cannot complete that SSO round-trip, and remo-web's
|
|
111
|
+
// strict `connect-src 'self'` CSP blocks following the redirect at all — so the
|
|
112
|
+
// only way to restore the session is a TOP-LEVEL navigation, which re-triggers
|
|
113
|
+
// the proxy's SSO flow (the browser CAN follow it through the IdP and back).
|
|
114
|
+
//
|
|
115
|
+
// `request()` uses `redirect: "manual"` so such a redirect surfaces as an opaque
|
|
116
|
+
// response (`response.type === "opaqueredirect"`, status 0) instead of throwing
|
|
117
|
+
// on the blocked cross-origin follow; we then reload the document to re-auth. A
|
|
118
|
+
// sessionStorage cooldown prevents a reload loop when auth genuinely can't
|
|
119
|
+
// complete. With no proxy (REMO_WEB_OPERATOR_AUTH=none) there are no redirects,
|
|
120
|
+
// so this path never fires.
|
|
121
|
+
|
|
122
|
+
const _REAUTH_KEY = "remo:last-reauth";
|
|
123
|
+
const _REAUTH_COOLDOWN_MS = 10_000;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle a forward-auth challenge on an XHR by re-authenticating through a
|
|
127
|
+
* top-level navigation. Never returns normally: it either navigates the whole
|
|
128
|
+
* document (throwing to halt the caller before the navigation lands) or, if we
|
|
129
|
+
* already tried to re-auth within the cooldown, throws a clear `auth_required`
|
|
130
|
+
* ApiError instead of looping.
|
|
131
|
+
*/
|
|
132
|
+
function reauthenticate(): never {
|
|
133
|
+
let last = 0;
|
|
134
|
+
try {
|
|
135
|
+
last = Number(sessionStorage.getItem(_REAUTH_KEY) ?? 0) || 0;
|
|
136
|
+
} catch {
|
|
137
|
+
last = 0;
|
|
138
|
+
}
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (now - last < _REAUTH_COOLDOWN_MS) {
|
|
141
|
+
// We just reloaded to re-auth and are being challenged again — the SSO
|
|
142
|
+
// round-trip isn't restoring a usable session. Stop reloading; surface a
|
|
143
|
+
// clear error rather than looping.
|
|
144
|
+
throw new ApiError({
|
|
145
|
+
code: "auth_required",
|
|
146
|
+
message: "Sign-in is required, but the access proxy did not restore a session.",
|
|
147
|
+
retryable: false,
|
|
148
|
+
remediation:
|
|
149
|
+
"Sign in through your access proxy and reload. If this repeats, the " +
|
|
150
|
+
"forward-auth proxy may be misconfigured (its session cookie is not reaching this app).",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
sessionStorage.setItem(_REAUTH_KEY, String(now));
|
|
155
|
+
} catch {
|
|
156
|
+
// sessionStorage unavailable — navigate anyway.
|
|
157
|
+
}
|
|
158
|
+
// A full document request re-runs the proxy's SSO redirect chain (IdP round
|
|
159
|
+
// trip), unlike a fetch which cannot. The SPA reloads authenticated and
|
|
160
|
+
// subsequent XHRs return 200.
|
|
161
|
+
window.location.assign(window.location.href);
|
|
162
|
+
// location.assign() schedules the navigation asynchronously and lets sync
|
|
163
|
+
// code keep running; throw so the caller never treats this as data.
|
|
164
|
+
throw new ApiError({
|
|
165
|
+
code: "auth_challenge",
|
|
166
|
+
message: "Re-authenticating…",
|
|
167
|
+
retryable: false,
|
|
168
|
+
remediation: "",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
104
172
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
105
173
|
let response: Response;
|
|
106
174
|
try {
|
|
107
175
|
response = await fetch(path, {
|
|
108
176
|
...init,
|
|
177
|
+
// Catch a forward-auth proxy's cross-origin SSO redirect as an opaque
|
|
178
|
+
// response instead of a thrown CSP-blocked follow (see reauthenticate()).
|
|
179
|
+
redirect: "manual",
|
|
109
180
|
headers: {
|
|
110
181
|
"Content-Type": "application/json",
|
|
111
182
|
...(init?.headers ?? {}),
|
|
@@ -123,6 +194,14 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
123
194
|
});
|
|
124
195
|
}
|
|
125
196
|
|
|
197
|
+
// A forward-auth session lapse: the proxy answered with a cross-origin 3xx,
|
|
198
|
+
// now an opaque redirect. Re-authenticate via a top-level navigation rather
|
|
199
|
+
// than surfacing a bogus "network_error"/CSP failure. The API itself never
|
|
200
|
+
// issues same-origin redirects, so this is unambiguously a proxy challenge.
|
|
201
|
+
if (response.type === "opaqueredirect") {
|
|
202
|
+
reauthenticate();
|
|
203
|
+
}
|
|
204
|
+
|
|
126
205
|
if (!response.ok) {
|
|
127
206
|
let envelope: ErrorEnvelope | undefined;
|
|
128
207
|
try {
|
|
@@ -220,6 +299,81 @@ export async function getReady(): Promise<ReadinessResponse> {
|
|
|
220
299
|
};
|
|
221
300
|
}
|
|
222
301
|
|
|
302
|
+
// ---- Pairing (012-web-adopt-pairing) ----
|
|
303
|
+
//
|
|
304
|
+
// The awaiting-adoption page (and the dashboard re-sync affordance) mints an
|
|
305
|
+
// ephemeral pairing code on open, which the operator copies to the clipboard
|
|
306
|
+
// and pastes into `remo web adopt` / `remo web push`. The code is returned only
|
|
307
|
+
// by mintPairingCode() at runtime — never embedded in the bundle (FR-016) — and
|
|
308
|
+
// the caller MUST hold it out of the DOM (copy-only, never displayed).
|
|
309
|
+
|
|
310
|
+
export interface MintPairingResponse {
|
|
311
|
+
code: string;
|
|
312
|
+
expires_in: number;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* `POST /api/v1/pairing/mint` — mint a fresh code (rotation-on-open, FR-003).
|
|
317
|
+
* `origin` distinguishes the adopt page from the dashboard re-sync affordance.
|
|
318
|
+
* A `403` means operator authentication is required/not configured (the page is
|
|
319
|
+
* reached through a forward-auth proxy in the gated posture) — surfaced via
|
|
320
|
+
* ApiError so the page can show guidance rather than a code.
|
|
321
|
+
*/
|
|
322
|
+
export async function mintPairingCode(
|
|
323
|
+
origin: "adopt" | "resync" = "adopt",
|
|
324
|
+
): Promise<MintPairingResponse> {
|
|
325
|
+
let response: Response;
|
|
326
|
+
try {
|
|
327
|
+
response = await fetch(`/api/v1/pairing/mint?origin=${encodeURIComponent(origin)}`, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
});
|
|
330
|
+
} catch (cause) {
|
|
331
|
+
throw new ApiError({
|
|
332
|
+
code: "network_error",
|
|
333
|
+
message: cause instanceof Error ? cause.message : "Network request failed",
|
|
334
|
+
retryable: true,
|
|
335
|
+
remediation: "Check that the Remo web service is reachable.",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (response.status === 403) {
|
|
339
|
+
// Operator authentication required / not configured — surface a distinct
|
|
340
|
+
// code so the adopt page can prompt to sign in rather than showing a code.
|
|
341
|
+
throw new ApiError({
|
|
342
|
+
code: "forbidden",
|
|
343
|
+
message: "Operator authentication is required to mint a pairing code.",
|
|
344
|
+
retryable: false,
|
|
345
|
+
remediation: "Sign in through your access proxy, then reload this page.",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw new ApiError({
|
|
350
|
+
code: "unknown",
|
|
351
|
+
message: `Mint failed with status ${response.status}`,
|
|
352
|
+
retryable: true,
|
|
353
|
+
remediation: "Reload this page to try again.",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return (await response.json()) as MintPairingResponse;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* `POST /api/v1/pairing/end` — best-effort session end (page-hide, FR-004).
|
|
361
|
+
* Uses `navigator.sendBeacon` when available so it survives page unload; the
|
|
362
|
+
* server treats it as idempotent and the idle TTL is the authoritative backstop.
|
|
363
|
+
*/
|
|
364
|
+
export function endPairing(): void {
|
|
365
|
+
const path = "/api/v1/pairing/end";
|
|
366
|
+
try {
|
|
367
|
+
if (navigator.sendBeacon?.(path)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// fall through to fetch
|
|
372
|
+
}
|
|
373
|
+
// keepalive lets the request outlive the page during unload.
|
|
374
|
+
void fetch(path, { method: "POST", keepalive: true }).catch(() => undefined);
|
|
375
|
+
}
|
|
376
|
+
|
|
223
377
|
// ---- Terminals (T041, US2) ----
|
|
224
378
|
//
|
|
225
379
|
// Per contracts/rest-api.md and contracts/terminal-websocket.md. The token
|
|
@@ -280,6 +434,12 @@ export async function closeTerminal(terminalId: string): Promise<void> {
|
|
|
280
434
|
*
|
|
281
435
|
* This function only constructs and returns the socket; connection-lifecycle
|
|
282
436
|
* and control-frame handling live in `terminal/TerminalConnection.ts`.
|
|
437
|
+
*
|
|
438
|
+
* Forward-auth note: a raw WebSocket upgrade cannot itself distinguish a proxy
|
|
439
|
+
* SSO redirect/401 from an ordinary failure. It does not need to — every attach
|
|
440
|
+
* (and reconnect) first calls `createTerminal()`, which goes through
|
|
441
|
+
* `request()` and so triggers the top-level SSO re-auth (see `reauthenticate`)
|
|
442
|
+
* whenever the session has lapsed, before/at the point the socket is opened.
|
|
283
443
|
*/
|
|
284
444
|
export function openTerminalSocket(terminalId: string, token: string): WebSocket {
|
|
285
445
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|