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