opsmith-cli 0.6.0__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/CHANGELOG.md +12 -0
  2. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/CLAUDE.md +1 -1
  3. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/PKG-INFO +2 -3
  4. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/notes/2026-09-04-migration-plan.md +9 -9
  5. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2a-schema-v2-and-upgrade.md +188 -0
  6. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2b-references-secrets-and-providers.md +154 -0
  7. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2c-compose-renderer-and-previews.md +91 -0
  8. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2d-deterministic-deploys.md +98 -0
  9. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2e-release-and-env-values.md +65 -0
  10. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2f-validation-repair-and-init-jobs.md +63 -0
  11. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2g-capacity-planning.md +145 -0
  12. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/2h-image-sources-and-deploy-flow.md +58 -0
  13. opsmith_cli-0.6.1/docs/specs/2026-09-04-phase-2-service-model-v2/README.md +115 -0
  14. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-3-customization.md +2 -1
  15. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-5-recipes.md +1 -1
  16. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-6-agentic-analysis.md +2 -2
  17. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/README.md +1 -1
  18. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/skill/SKILL.md +1 -1
  19. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_run/aws/main.yml +12 -0
  20. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_run/gcp/main.yml +7 -0
  21. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_provisioners.py +41 -0
  22. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/pyproject.toml +2 -3
  23. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/uv.lock +1 -1
  24. opsmith_cli-0.6.0/docs/specs/2026-09-04-phase-2-service-model-v2.md +0 -439
  25. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/.claude/settings.json +0 -0
  26. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/.flake8 +0 -0
  27. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/.github/workflows/python-publish.yml +0 -0
  28. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/.github/workflows/tests.yml +0 -0
  29. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/.gitignore +0 -0
  30. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/.pre-commit-config.yaml +0 -0
  31. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/LICENSE.txt +0 -0
  32. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/README.md +0 -0
  33. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/README.md +0 -0
  34. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/notes/README.md +0 -0
  35. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/reference/2026-09-21-agent-skill.md +0 -0
  36. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/reference/README.md +0 -0
  37. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0a-cli-split-and-errors.md +0 -0
  38. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0b-context-and-events.md +0 -0
  39. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0c-model-config-and-tool-checks.md +0 -0
  40. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0d-interaction-api.md +0 -0
  41. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0e-headless-mode.md +0 -0
  42. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0f-headless-subcommands.md +0 -0
  43. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/0g-provider-questions-and-plan.md +0 -0
  44. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-0-headless-core/README.md +0 -0
  45. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-1-harness-integration.md +0 -0
  46. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-4-remote-state.md +0 -0
  47. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-7-mcp-server.md +0 -0
  48. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/docs/specs/2026-09-04-phase-8-data-durability.md +0 -0
  49. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/__init__.py +0 -0
  50. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/agent.py +0 -0
  51. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/__init__.py +0 -0
  52. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/agent_install.py +0 -0
  53. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/app.py +0 -0
  54. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/__init__.py +0 -0
  55. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/agent.py +0 -0
  56. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/analyze.py +0 -0
  57. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/config.py +0 -0
  58. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/deploy.py +0 -0
  59. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/env.py +0 -0
  60. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/release.py +0 -0
  61. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/setup.py +0 -0
  62. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/commands/validate.py +0 -0
  63. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/flags.py +0 -0
  64. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/interaction.py +0 -0
  65. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/output.py +0 -0
  66. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/skill_refs.py +0 -0
  67. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cli/state.py +0 -0
  68. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cloud_providers/__init__.py +0 -0
  69. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cloud_providers/aws.py +0 -0
  70. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cloud_providers/base.py +0 -0
  71. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/cloud_providers/gcp.py +0 -0
  72. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/constants.py +0 -0
  73. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/__init__.py +0 -0
  74. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/answers.py +0 -0
  75. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/config.py +0 -0
  76. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/context.py +0 -0
  77. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/errors.py +0 -0
  78. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/events.py +0 -0
  79. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/interaction.py +0 -0
  80. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/llm.py +0 -0
  81. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/operations.py +0 -0
  82. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/provisioners.py +0 -0
  83. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/questions.py +0 -0
  84. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/results.py +0 -0
  85. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/core/steps.py +0 -0
  86. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/deployment_strategies/__init__.py +0 -0
  87. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/deployment_strategies/base.py +0 -0
  88. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/deployment_strategies/monolithic.py +0 -0
  89. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/git_repo.py +0 -0
  90. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/infra_provisioners/__init__.py +0 -0
  91. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/infra_provisioners/ansible_provisioner.py +0 -0
  92. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/infra_provisioners/base_provisioner.py +0 -0
  93. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/infra_provisioners/terraform_provisioner.py +0 -0
  94. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/main.py +0 -0
  95. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/models.py +0 -0
  96. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/prompts.py +0 -0
  97. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/README.md +0 -0
  98. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/arduino-tags.scm +0 -0
  99. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/c-tags.scm +0 -0
  100. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/c_sharp-tags.scm +0 -0
  101. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/chatito-tags.scm +0 -0
  102. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/commonlisp-tags.scm +0 -0
  103. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/cpp-tags.scm +0 -0
  104. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/csharp-tags.scm +0 -0
  105. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/d-tags.scm +0 -0
  106. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/dart-tags.scm +0 -0
  107. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/elisp-tags.scm +0 -0
  108. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/elixir-tags.scm +0 -0
  109. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/elm-tags.scm +0 -0
  110. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/gleam-tags.scm +0 -0
  111. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/go-tags.scm +0 -0
  112. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/hcl-tags.scm +0 -0
  113. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/java-tags.scm +0 -0
  114. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/javascript-tags.scm +0 -0
  115. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/kotlin-tags.scm +0 -0
  116. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/lua-tags.scm +0 -0
  117. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/ocaml-tags.scm +0 -0
  118. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/ocaml_interface-tags.scm +0 -0
  119. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/php-tags.scm +0 -0
  120. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/pony-tags.scm +0 -0
  121. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/properties-tags.scm +0 -0
  122. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/python-tags.scm +0 -0
  123. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/ql-tags.scm +0 -0
  124. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/r-tags.scm +0 -0
  125. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/racket-tags.scm +0 -0
  126. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/ruby-tags.scm +0 -0
  127. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/rust-tags.scm +0 -0
  128. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/scala-tags.scm +0 -0
  129. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/solidity-tags.scm +0 -0
  130. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/swift-tags.scm +0 -0
  131. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/typescript-tags.scm +0 -0
  132. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/queries/tree-sitter-languages/udev-tags.scm +0 -0
  133. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/repo_map.py +0 -0
  134. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/service_detector.py +0 -0
  135. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/settings.py +0 -0
  136. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/skill/references/commands.md +0 -0
  137. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/skill/references/config-schema.md +0 -0
  138. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/skill/references/ownership.md +0 -0
  139. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/skill/references/workflows.md +0 -0
  140. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/cloud_storage_cleanup/aws/main.yml +0 -0
  141. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/cloud_storage_cleanup/gcp/main.yml +0 -0
  142. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/container_registry/aws/main.tf +0 -0
  143. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/container_registry/aws/outputs.tf +0 -0
  144. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/container_registry/aws/variables.tf +0 -0
  145. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/container_registry/gcp/main.tf +0 -0
  146. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/container_registry/gcp/outputs.tf +0 -0
  147. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/container_registry/gcp/variables.tf +0 -0
  148. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_build_push/aws/main.yml +0 -0
  149. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_build_push/gcp/main.yml +0 -0
  150. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_deploy/aws/inventory.yml +0 -0
  151. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_deploy/aws/main.yml +0 -0
  152. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_deploy/gcp/inventory.yml +0 -0
  153. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_deploy/gcp/main.yml +0 -0
  154. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_run/aws/inventory.yml +0 -0
  155. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_run/gcp/inventory.yml +0 -0
  156. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/base.yml +0 -0
  157. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/elasticsearch.yml +0 -0
  158. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/kafka.yml +0 -0
  159. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/mongodb.yml +0 -0
  160. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/mysql.yml +0 -0
  161. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/postgresql.yml +0 -0
  162. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/rabbitmq.yml +0 -0
  163. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/redis.yml +0 -0
  164. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/services/backend_api.yml +0 -0
  165. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/services/backend_worker.yml +0 -0
  166. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/services/full_stack.yml +0 -0
  167. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/traefik.yml +0 -0
  168. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/docker_compose_snippets/weaviate.yml +0 -0
  169. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/dockerfiles/python_backend_api +0 -0
  170. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/dockerfiles/python_backend_worker +0 -0
  171. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/fetch_remote_files/aws/inventory.yml +0 -0
  172. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/fetch_remote_files/aws/main.yml +0 -0
  173. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/fetch_remote_files/gcp/inventory.yml +0 -0
  174. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/fetch_remote_files/gcp/main.yml +0 -0
  175. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_bucket_cert/aws/main.tf +0 -0
  176. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_bucket_cert/aws/outputs.tf +0 -0
  177. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_bucket_cert/aws/variables.tf +0 -0
  178. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_bucket_cert/gcp/main.tf +0 -0
  179. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_bucket_cert/gcp/outputs.tf +0 -0
  180. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_bucket_cert/gcp/variables.tf +0 -0
  181. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_cdn/aws/main.tf +0 -0
  182. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_cdn/aws/outputs.tf +0 -0
  183. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_cdn/aws/variables.tf +0 -0
  184. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_cdn/gcp/main.tf +0 -0
  185. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_cdn/gcp/outputs.tf +0 -0
  186. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_cdn/gcp/variables.tf +0 -0
  187. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_deploy/aws/main.yml +0 -0
  188. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/frontend_deploy/gcp/main.yml +0 -0
  189. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine/aws/main.tf +0 -0
  190. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine/aws/outputs.tf +0 -0
  191. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine/aws/variables.tf +0 -0
  192. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine/gcp/main.tf +0 -0
  193. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine/gcp/outputs.tf +0 -0
  194. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine/gcp/variables.tf +0 -0
  195. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine_setup/aws/inventory.yml +0 -0
  196. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine_setup/aws/main.yml +0 -0
  197. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine_setup/gcp/inventory.yml +0 -0
  198. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/templates/virtual_machine_setup/gcp/main.yml +0 -0
  199. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/__init__.py +0 -0
  200. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/conftest.py +0 -0
  201. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_agent_install.py +0 -0
  202. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_answers.py +0 -0
  203. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_boundaries.py +0 -0
  204. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_cli_contract.py +0 -0
  205. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_cloud_providers.py +0 -0
  206. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_commands.py +0 -0
  207. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_config_commands.py +0 -0
  208. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_env_plan.py +0 -0
  209. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_error_hints.py +0 -0
  210. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_events.py +0 -0
  211. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_flag_mapping.py +0 -0
  212. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_git_repo.py +0 -0
  213. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_interaction.py +0 -0
  214. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_interaction_keys.py +0 -0
  215. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_llm_config.py +0 -0
  216. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_monolithic_strategy.py +0 -0
  217. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_operations.py +0 -0
  218. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_questions.py +0 -0
  219. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_requirements.py +0 -0
  220. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_service_detector.py +0 -0
  221. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_skill_refs.py +0 -0
  222. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_steps.py +0 -0
  223. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/tests/test_validate_commands.py +0 -0
  224. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/types.py +0 -0
  225. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/opsmith/utils.py +0 -0
  226. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/resources/deploy_demo.gif +0 -0
  227. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/resources/deploy_demo.svg +0 -0
  228. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/resources/setup_demo.gif +0 -0
  229. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/resources/setup_demo.svg +0 -0
  230. {opsmith_cli-0.6.0 → opsmith_cli-0.6.1}/scripts/build_skill_refs.py +0 -0
@@ -4,6 +4,18 @@ All notable changes to Opsmith are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
5
5
  [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.1] - 2026-10-01
8
+
9
+ ### Fixed
10
+
11
+ - `opsmith run` no longer runs a slow command more than once on AWS. The command was held open
12
+ as one SSM execution. Any command that took longer than a minute hit the connection's timeout,
13
+ and the connection then executed it again from the start, up to three more times, while the
14
+ earlier runs carried on. The command now starts as a background job on the machine, and
15
+ Opsmith checks on it every ten seconds until it finishes. Starting the job is never retried. A
16
+ command may run for up to 24 hours, after which it is stopped. GCP runs the same way, so a
17
+ dropped IAP tunnel no longer ends the run.
18
+
7
19
  ## [0.6.0] - 2026-09-21
8
20
 
9
21
  Coding-harness integration, phase 1 of the
@@ -254,7 +254,7 @@ anything structural.
254
254
  ownership, the testing standard.
255
255
  - `docs/specs/` — nine phase specs. Phase 0 (headless core) is split into seven parts, `0a`–`0g`,
256
256
  each its own merge unit; its `README.md` is the index and records which acceptance criterion each
257
- part proves.
257
+ part proves. Phase 2 is split the same way, into eight parts, `2a`–`2h`.
258
258
  - `docs/reference/` — how the system works today. Written in the present tense, maintained.
259
259
 
260
260
  A spec is written before the work and stops changing once it ships; do not amend one to match what
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opsmith-cli
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Opsmith is an AI devops engineer in your terminal
5
5
  License-Expression: GPL-3.0-only
6
6
  License-File: LICENSE.txt
@@ -9,10 +9,9 @@ Classifier: Environment :: Console
9
9
  Classifier: Intended Audience :: Developers
10
10
  Classifier: Programming Language :: Python
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
12
  Classifier: Programming Language :: Python :: 3.12
15
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
16
15
  Classifier: Topic :: Software Development
17
16
  Requires-Python: >=3.12
18
17
  Requires-Dist: ansible==14.3.1
@@ -10,7 +10,7 @@ The specs live in [`../specs/`](../specs/), one per phase:
10
10
  |------:|------|------------|------|----------|
11
11
  | 0 | [Headless core](../specs/2026-09-04-phase-0-headless-core/) | – | L | 0.5.0, shipped |
12
12
  | 1 | [Coding-harness integration](../specs/2026-09-04-phase-1-harness-integration.md) | 0 | M | 0.6.0 |
13
- | 2 | [Service model v2 and deterministic rendering](../specs/2026-09-04-phase-2-service-model-v2.md) | 0 | XL | 1.0.0 |
13
+ | 2 | [Service model v2 and deterministic rendering](../specs/2026-09-04-phase-2-service-model-v2/) | 0 | XL | 1.0.0 |
14
14
  | 3 | [Customization layer](../specs/2026-09-04-phase-3-customization.md) | 0, 2 | M | 1.0.0, with phase 2 |
15
15
  | 4 | [Remote state and config sync](../specs/2026-09-04-phase-4-remote-state.md) | 0; 3 recommended first | M | 1.1.0 |
16
16
  | 5 | [Recipes](../specs/2026-09-04-phase-5-recipes.md) | 2, 3, 4 | L | 1.2.0 |
@@ -22,7 +22,7 @@ Phase 0 is specified as seven parts inside [its spec folder](../specs/2026-09-04
22
22
 
23
23
  **The phases were renumbered on 2026-09-20**, when harness integration moved from last to first. A phase number is its position here, so moving the work meant moving the number; the map from the old numbers is under [renumbering](#renumbering), and the shipped phase 0 specs still use the old ones.
24
24
 
25
- Sizes are relative effort, not dates. Phases 4 and 6 can run in parallel with their neighbours once their dependencies have landed. Phases 2 and 3 ship in one release: the first deterministic release overwrites hand-edited compose files, and phase 3 provides the override file where those edits belong.
25
+ Sizes are relative effort, not dates. Phases 4 and 6 can run in parallel with their neighbours once their dependencies have landed. Phases 2 and 3 ship in one release: the first `release` or `update` after upgrading overwrites compose files edited on the machine, and phase 3 provides the override file where those edits belong.
26
26
 
27
27
  Harness integration goes first because almost all of it is already possible: the skill is instructions, its two generated references come from the Typer app and the pydantic schema that phase 0 already exposes, and the installer is file copying. What it cannot describe yet — the ownership manifest, the reference grammar, recipes, the template registry — is added by the phase that builds each one, under [definition of done](#definition-of-done-for-a-phase). The alternative was shipping the agent surface last, which would mean the tool spent its whole 0.x series being the thing agents could not drive.
28
28
 
@@ -53,7 +53,7 @@ without it.
53
53
 
54
54
  Every one of the planned capabilities is blocked by the same properties of the code today:
55
55
 
56
- 1. **The model is used where code belongs.** `MonolithicDeploymentStrategy._generate_docker_compose` asks it to merge compose snippets and invent env values on every attempt, `_deploy_validate_docker_compose` asks it to judge success from container logs on every release, and `_select_virtual_machine_type` asks it to size a VM with no declared resource needs. Those steps are rendering and arithmetic, not judgment: they are slow, non-deterministic, untestable, and tied to one strategy. The model stays where judgment is needed, on analysis, generation, estimation and diagnosis.
56
+ 1. **The model is used where code belongs.** `MonolithicDeploymentStrategy._generate_docker_compose` asks it to merge compose snippets and invent env values on every attempt, and `_select_virtual_machine_type` asks it to size a VM with no declared resource needs. Those steps are rendering and arithmetic, not judgment: they are slow, non-deterministic, untestable, and tied to one strategy. The model stays where judgment is needed, on analysis, generation, estimation, diagnosis and deciding whether a deployment came up, though `_deploy_validate_docker_compose` has it decide that from container logs alone, with no container states to hold it to.
57
57
  2. **Every flow is interactive.** `inquirer` prompts live in `main.py`, `service_detector.py`, `deployment_strategies/monolithic.py` and inside `cloud_providers/aws.py` and `gcp.py`. An agent driving opsmith through a shell cannot answer them, and nothing can be tested end to end.
58
58
  3. **State is local-only.** Terraform state is gitignored by `GitRepo.ensure_gitignore`, so git never protected it; the only copy is on the user's disk.
59
59
  4. **Analysis is a pre-computed map.** `repo_map.py` is an aider port with the ranking removed, capped at a small token budget, and regenerated on every Dockerfile attempt.
@@ -84,7 +84,7 @@ Principles that hold across phases:
84
84
 
85
85
  - **Core is UI-free.** Nothing outside `opsmith/cli/` imports `inquirer`, `typer`, or `rich` prompt helpers. A test enforces it from phase 0 on.
86
86
  - **Every interaction has a key.** Strategies ask, confirm, wait and notify through one interaction API with stable keys, so answers can come from a terminal, flags, a file, env vars or a driving agent, and every command is safe to run again after it stops for an answer or an external action.
87
- - **The model does the judgment, code does the rendering.** A configured model is a requirement of the tool, not an option. It analyses repositories, generates Dockerfiles, estimates what the config does not declare, and explains failures. Rendering artifacts and checking that a deployment came up are deterministic code, so results are reproducible and testable. A coding harness may also author config and Dockerfiles itself and have opsmith validate them.
87
+ - **The model does the judgment, code does the rendering.** A configured model is a requirement of the tool, not an option. It analyses repositories, generates Dockerfiles, estimates what the config does not declare, judges whether a deployment came up, and repairs the compose file when that is what failed. Rendering artifacts is deterministic code, so results are reproducible and testable. The judgment works over facts that code gathers (container states, health, restarts), and it cannot overrule them to call a failed container a success. A coding harness may also author config and Dockerfiles itself and have opsmith validate them.
88
88
  - **Strategies render and plan, the model estimates.** Given the service model, the bindings and a capacity estimate, a strategy produces artifacts and a machine plan deterministically. Same input, same output. How many machines and of what kind is the strategy's decision: one VM for monolithic, node pools for a future Kubernetes strategy.
89
89
  - **Recipes are configuration, not a new format.** A recipe is a partial `deployments.yml` that any strategy can render.
90
90
  - **State lives in the user's cloud account.** A per-app bucket holds Terraform state and a copy of the config, with locking.
@@ -187,7 +187,7 @@ Stable keys used by the interaction API. Flags map onto these, and an answers fi
187
187
  | `env.domain_email` | `env create`, `update` | `--domain-email` |
188
188
  | `env.domain.<slug>` | `env create`, `update` | `--domain slug=host` |
189
189
  | `env.state_backend` | `env create` (phase 4) | `--state cloud\|local` |
190
- | `envvar.<KEY>` | compose env confirmation | `--env-var KEY=VALUE` |
190
+ | `envvar.<KEY>` | compose env confirmation; `env vars` (phase 2) | `--env-var KEY=VALUE` |
191
191
  | `build_env.<slug>.<KEY>` | frontend build env | `--build-env slug:KEY=VALUE` |
192
192
  | `dns.confirm` | the DNS confirmation as it stands today, over every record at once | `--answer dns.confirm=true` |
193
193
  | `dns.<slug>` | `wait_for` once the records are known, replacing `dns.confirm` | run again after creating the records; answered by key only where the strategy cannot verify |
@@ -195,8 +195,8 @@ Stable keys used by the interaction API. Flags map onto these, and an answers fi
195
195
  | `run.service`, `run.command` | `run` | positional |
196
196
  | `delete.confirm` | `destroy` | `--answer delete.confirm=DELETE` |
197
197
  | `update.confirm_infra_changes` | `update` | `--answer update.confirm_infra_changes=true` |
198
- | `dockerfile.edit`, `compose.edit` | fix editors | exit 4 headless, with the path and the validate command |
199
- | `config.upgrade` | first save after schema upgrade (phase 2) | `--answer config.upgrade=true` |
198
+ | `update.confirm_compose_upgrade` | `update`, on the first render of an environment deployed before phase 2, when that render would drop something (phase 2) | `--answer update.confirm_compose_upgrade=true` |
199
+ | `dockerfile.edit`, `compose.edit` | fix editors | exit 4 headless, with the path and the validate command || `config.upgrade` | first save after schema upgrade (phase 2) | `--answer config.upgrade=true` |
200
200
  | `recipe.input.<KEY>` | `recipe add` (phase 5) | `--input KEY=VALUE` |
201
201
  | `template.reset.confirm`, `template.adopt.confirm` | `template reset`, `template adopt` (phase 3) | `--answer template.reset.confirm=true`, `--answer template.adopt.confirm=true` |
202
202
 
@@ -252,7 +252,7 @@ directory, or a build context that never read it.
252
252
 
253
253
  - **Rewriting the CLI in JavaScript.** Ansible is pip-installed today and would become a user prerequisite; after phase 0 the UI layer is thin and its language barely matters. Revisit only if Ansible is replaced.
254
254
  - **Compose files as the recipe format.** Recipes are expressed in the `deployments.yml` schema so any strategy can render them.
255
- - **LLM-rendered docker-compose.** Rendering becomes deterministic in phase 2.
255
+ - **LLM-rendered docker-compose.** Rendering becomes deterministic in phase 2. After a failed deploy the model may repair the deployed file, which releases keep deploying until the next `update` renders from the config again; how a repair survives that is left to phase 3.
256
256
  - **An LLM-free mode.** The model is part of the tool and is always configured. Steps that are pure rendering or arithmetic are deterministic for reproducibility, not to remove the model.
257
257
  - **A Kubernetes strategy.** Out of scope, but the service model and the capacity-planning hook in phase 2 are designed so one can be added without schema changes: it would implement `plan_capacity` to return node pools instead of one VM.
258
258
 
@@ -281,9 +281,9 @@ names "phase 5". Read every one of them through the left column of this table.
281
281
 
282
282
  Tracked here, resolved inside the phase that owns them.
283
283
 
284
- - Phase 2: whether `FULL_STACK` remains a distinct service type or becomes `BACKEND_API` with a route on `/`.
285
284
  - Phase 2: replicas under the monolithic strategy. Current answer: honoured through compose `deploy.replicas` for services without volumes; services with volumes stay at one replica.
286
285
  - Phase 4: one state bucket per cloud account versus one per environment. Current answer: per account, keyed by app slug.
287
286
  - Phase 5: policy for recipe slugs colliding with detected services. Current answer: fail with a `--slug-prefix` hint.
288
287
  - Phase 1: exact skill directories per harness at implementation time; conventions are still moving.
289
288
  - Phase 5: whether recipes may ship overlays or override files. Current answer: no, recipes use `files/` only.
289
+ - Phase 3: how the model's repair of a failed deploy (phase 2) survives an `update`, how a fix whose cause is in `deployments.yml` is proposed, confirmed and applied there, and whether the working directory's compose file stays committed now that `release` deploys it (a checkout with an older copy would deploy that). Current answer: the repair is written to that file, which releases deploy until the next `update` renders from the config, and its notice names the field.
@@ -0,0 +1,188 @@
1
+ # Phase 2a: Schema v2 and the v1 upgrade
2
+
3
+ **Goal:** `deployments.yml` gains schema version 2, with every field the later parts render, the renamed service types and the structural validation rules, and a v1 config is upgraded in memory on load and rewritten on the first save.
4
+ **Depends on:** phase 0.
5
+ **Size:** M.
6
+ **Lands as:** the first part of phase 2; the phase ships as 1.0.0 with phase 3 once 2h lands.
7
+
8
+ ## Scope
9
+
10
+ 1. The schema v2 models in `opsmith/types.py`, and `schema_version`.
11
+ 2. The service types renamed, with `FULL_STACK` merged into `WEB_SERVICE`.
12
+ 3. The structural validation rules, enforced by `config validate`.
13
+ 4. `upgrade_config`, the upgrade of deployed snapshots, and the first save with a backup.
14
+ 5. Change detection over the whole service and infra models.
15
+
16
+ ## Non-goals
17
+
18
+ - Honouring the new fields. Routes, volumes, files, healthchecks, init jobs, resources and image sources are accepted and validated here, and take effect in the part that renders or runs them: 2c and 2d, 2f, 2g and 2h.
19
+ - References and `secret()` (2b), and detection writing them (2d).
20
+
21
+ ## Design
22
+
23
+ ### Schema v2
24
+
25
+ `DeploymentConfig` gains `schema_version: int = 2`. New and changed models in `opsmith/types.py`:
26
+
27
+ ```python
28
+ class BuildSource(BaseModel):
29
+ kind: Literal["build"] = "build"
30
+ context: str = "." # build context relative to repo root
31
+ dockerfile: Optional[str] = None # default .opsmith/docker/<slug>/Dockerfile
32
+
33
+ class ImageSource(BaseModel):
34
+ kind: Literal["image"] = "image"
35
+ image: str # "odoo", "ghcr.io/org/app"
36
+ tag: str = "latest"
37
+ platforms: List[str] = ["linux/amd64"] # used for architecture selection
38
+
39
+ class Route(BaseModel):
40
+ path_prefix: str = "/"
41
+ port: int
42
+ strip_prefix: bool = False
43
+
44
+ class VolumeMount(BaseModel):
45
+ name: str # [a-z0-9_]+, unique per config
46
+ path: str # absolute path inside the container
47
+ backup: bool = False # consumed by phase 8
48
+
49
+ class FileMount(BaseModel):
50
+ path: str # absolute path inside the container
51
+ content: Optional[str] = None # template, resolved with the render context
52
+ source: Optional[str] = None # path under .opsmith/files/ or a recipe's files/ dir; exclusive with content
53
+ mode: str = "0644"
54
+
55
+ class HealthCheck(BaseModel):
56
+ http_path: Optional[str] = None
57
+ cmd: Optional[List[str]] = None # exclusive with http_path
58
+ port: Optional[int] = None
59
+ interval_s: int = 10
60
+ timeout_s: int = 5
61
+ retries: int = 5
62
+ start_period_s: int = 30
63
+
64
+ class Resources(BaseModel):
65
+ """What one instance of the service needs at light load. Scaled per environment by the capacity plan."""
66
+ min_ram_gb: float
67
+ recommended_ram_gb: Optional[float] = None
68
+ min_cpu: float = 0.25
69
+ storage_gb: Optional[float] = None # for services with volumes
70
+
71
+ class InitJob(BaseModel):
72
+ name: str
73
+ command: List[str]
74
+ when: Literal["first_deploy", "every_release"] = "every_release"
75
+
76
+ class EnvVarConfig(BaseModel):
77
+ key: str
78
+ is_secret: bool = False
79
+ value: Optional[str] = None # template; resolved at render time; never prompted
80
+ default_value: Optional[str] = None # prompted with this default when value is None
81
+
82
+ class ServiceInfo(BaseModel):
83
+ name_slug: str
84
+ source: Union[BuildSource, ImageSource] = Field(default_factory=BuildSource, discriminator="kind")
85
+ service_type: ServiceTypeEnum
86
+ language: Optional[str] = None # required when source.kind == "build"
87
+ language_version: Optional[str] = None
88
+ framework: Optional[str] = None
89
+ service_port: Optional[int] = None
90
+ build_cmd / build_dir / build_path # unchanged, STATIC_SITE only
91
+ routes: List[Route] = []
92
+ env_vars: List[EnvVarConfig] = []
93
+ volumes: List[VolumeMount] = []
94
+ files: List[FileMount] = []
95
+ command: Optional[List[str]] = None
96
+ entrypoint: Optional[List[str]] = None
97
+ healthcheck: Optional[HealthCheck] = None
98
+ resources: Optional[Resources] = None
99
+ init_jobs: List[InitJob] = []
100
+ depends_on: List[str] = [] # service slugs or infra instance names
101
+
102
+ class InfrastructureDependency(BaseModel):
103
+ instance: str # default: provider value; unique per config
104
+ dependency_type: DependencyTypeEnum
105
+ provider: InfrastructureProviderEnum
106
+ version: str = "latest"
107
+ settings: Dict[str, str] = {} # provider-specific: username, database, extra env
108
+ ```
109
+
110
+ `ServiceTypeEnum` is renamed for what each type still decides once `routes` carry exposure. `FULL_STACK` is merged into the type that replaces `BACKEND_API`, because the two already rendered the same way and no Dockerfile template told them apart:
111
+
112
+ | v1 | v2 | Meaning |
113
+ |----|----|---------|
114
+ | `BACKEND_API`, `FULL_STACK` | `WEB_SERVICE` | a container that listens on a port and serves requests, whether it is exposed through routes or only reached by other services |
115
+ | `BACKEND_WORKER` | `WORKER` | a container that listens on no port, such as a queue consumer or a scheduler |
116
+ | `FRONTEND` | `STATIC_SITE` | built into static files and served from a CDN, with no container |
117
+
118
+ - The meanings go into the `service_type` field's description, which detection reads through the generated field list. So an internal API is a `WEB_SERVICE` although it is not public, and a Next.js or Nuxt app that renders on a server is a `WEB_SERVICE`, not a `STATIC_SITE`.
119
+ - The type selects the Dockerfile template, and `python_backend_api` and `python_backend_worker` become `python_web_service` and `python_worker`. For a `STATIC_SITE` it also selects the CDN path.
120
+ - Slugs are not renamed. Detection builds them from the type, so new services get names such as `python_web_service_1`, but an existing slug names compose services, Dockerfile directories and CDN state, and stays as it is.
121
+ - Event step names such as `frontend` are part of the event schema, and keep their names too.
122
+
123
+ ### Validation rules
124
+
125
+ Enforced by `config validate`. [2b](2b-references-secrets-and-providers.md) adds the three rules about references, `secret()` and duplicate env var keys.
126
+
127
+ - A service is **exposed** iff `routes` is non-empty. `STATIC_SITE` services never have routes or containers; they keep the CDN path.
128
+ - `route.port` must be `service_port` or another port the container serves; duplicates of `path_prefix` within a service are errors.
129
+ - `volumes[].name` unique across the config; `files[].path` unique per service; `content` xor `source`.
130
+ - `depends_on` entries must exist as a service slug or an infra instance.
131
+ - `language` required for build sources.
132
+ - Infra `instance` unique; `provider` must be compatible with `dependency_type` (existing `COMPATIBLE_PROVIDERS`).
133
+
134
+ ### Upgrade from v1
135
+
136
+ `opsmith/core/config.py::upgrade_config(data: dict) -> dict`, applied on load when `schema_version` is missing:
137
+
138
+ - every service gets `source: {kind: build}`;
139
+ - `BACKEND_API` and `FULL_STACK` with `service_port` get `routes: [{path_prefix: "/", port: service_port}]`; `BACKEND_WORKER` gets none;
140
+ - `service_type` is renamed as in the table above: `BACKEND_API` and `FULL_STACK` become `WEB_SERVICE`, `BACKEND_WORKER` becomes `WORKER`, and `FRONTEND` becomes `STATIC_SITE`. Slugs are left alone;
141
+ - infra deps get `instance = provider`;
142
+ - every container service gets `depends_on` listing every infra instance, which is what the LLM-generated compose files declared;
143
+ - env vars keep `default_value`; no `value` is invented.
144
+ - `MonolithicDeploymentState.deployed_services` snapshots are upgraded the same way on load so change detection does not report a spurious full change.
145
+
146
+ The first save after an upgrade asks `config.upgrade` (`--answer config.upgrade=true` headless) and writes `.opsmith/deployments.v1.bak.yml`.
147
+
148
+ ### Change detection
149
+
150
+ `_detect_configuration_changes` compares the full `model_dump` of each service and infra instance, keyed by slug and instance, and reports changed field names. Because the deployed snapshots are upgraded the same way as the config, an environment deployed before this part reports no spurious change. [2d](2d-deterministic-deploys.md) adds the compose renderer's version to what counts as a change.
151
+
152
+ ### Until 2d: the LLM compose path
153
+
154
+ The model still writes the compose file until 2d replaces it, from per-type snippets that it looks up by `service_type`. So the snippets are renamed with the types: `services/backend_api.yml` becomes `services/web_service.yml`, `services/backend_worker.yml` becomes `services/worker.yml`, and `services/full_stack.yml`, which differed from the API snippet only by a trailing blank line, is deleted. Domains are still asked for by type, now `WEB_SERVICE` and `STATIC_SITE`, until 2d asks by routes.
155
+
156
+ ### Detection
157
+
158
+ The hand-written field list in `REPO_ANALYSIS_PROMPT_TEMPLATE` gets the new type names with their meanings, and asks for `routes` on web services, so a freshly detected config is exposed the same way an upgraded one is. 2d replaces the list with one generated from the schema, together with the instructions for references and `secret()`.
159
+
160
+ ## Code changes by file
161
+
162
+ | File | Change |
163
+ |------|--------|
164
+ | `opsmith/types.py` | the v2 models; `schema_version`; `ServiceTypeEnum` renamed, with `FULL_STACK` merged; structural validators; deployed snapshots upgraded on load |
165
+ | `opsmith/core/config.py` | load with upgrade, save with backup |
166
+ | `opsmith/deployment_strategies/monolithic.py` | the type renames; change detection over the full models |
167
+ | `opsmith/core/operations.py`, `opsmith/service_detector.py` | the type renames, including `ROUTED_SERVICE_TYPES`, `BUILDABLE_SERVICE_TYPES` and the slugs detection builds |
168
+ | `opsmith/templates/docker_compose_snippets/services/` | the per-type snippets renamed; `full_stack.yml` deleted |
169
+ | `opsmith/templates/dockerfiles/` | `python_backend_api` and `python_backend_worker` renamed to `python_web_service` and `python_worker` |
170
+ | `opsmith/prompts.py` | the detection field list's type names and `routes` |
171
+ | `README.md` | schema v2 documentation |
172
+
173
+ ## Acceptance criteria
174
+
175
+ 1. A v1 project loads, validates, and deploys without edits; the first save writes a v2 file and a backup (phase acceptance criterion 1, which every later part keeps true).
176
+ 2. An environment deployed before this part reports no service or infra changes on `update`.
177
+ 3. `config validate` rejects a violation of each structural rule with the path to the offending field.
178
+
179
+ ## Tests
180
+
181
+ - `test_types_v2.py`: validators, discriminator, uniqueness rules.
182
+ - `test_config_upgrade.py`: v1 fixtures upgrade to expected v2 dicts, including the type renames and the `FULL_STACK` merge with slugs unchanged; backup written once.
183
+ - Change detection: an upgraded config against its upgraded snapshots reports no change.
184
+
185
+ ## Harness surface
186
+
187
+ - `references/config-schema.md` regenerates for schema v2. The freshness test fails until it is committed.
188
+ - `SKILL.md`: the hand-authoring section gets the renamed service types. The skill must say that a v1 config is upgraded in memory and rewritten on the next save, because a harness will meet both shapes in repositories it did not write.
@@ -0,0 +1,154 @@
1
+ # Phase 2b: References, secrets and provider specs
2
+
3
+ **Goal:** env values and file contents can reference infrastructure, services and domains through one grammar, app secrets are asked for with `secret()`, and what opsmith knows about each infra provider lives in one registry, all checked by `config validate` without a strategy.
4
+ **Depends on:** 2a.
5
+ **Size:** M.
6
+ **Lands as:** a part of phase 2; the phase ships as 1.0.0 with phase 3 once 2h lands.
7
+
8
+ ## Scope
9
+
10
+ 1. The reference grammar and the resolvers in `opsmith/core/render.py`.
11
+ 2. `secret()` and its formats, and the rules for which secrets are generated.
12
+ 3. The infra provider registry, `opsmith/infra/providers.py`.
13
+ 4. The monolithic bindings.
14
+ 5. The validation rules for references, `secret()` and duplicate env var keys.
15
+
16
+ ## Non-goals
17
+
18
+ - Rendering ([2c](2c-compose-renderer-and-previews.md)), and generating or asking for anything during a deploy ([2d](2d-deterministic-deploys.md)). This part is functions and their tests; no command deploys with them yet.
19
+ - Detection writing references or `secret()` (2d). Until then only a hand-written config holds them, and `config validate` accepts them.
20
+
21
+ ## Design
22
+
23
+ ### Reference grammar and bindings
24
+
25
+ Templates use Jinja2 with `StrictUndefined`, expression-only, plus one function, `secret(length=32, format='alnum')`, described below. Allowed roots:
26
+
27
+ | Root | Attributes | Bound by |
28
+ |------|-----------|----------|
29
+ | `infra.<instance>` | `host`, `port`, `username`, `password`, `database`, `url` | strategy |
30
+ | `services.<slug>` | `host`, `port` | strategy |
31
+ | `domains.<slug>` | string | environment config |
32
+ | `inputs.<KEY>` | string | recipe inputs (phase 5); empty until then |
33
+ | `env` | environment name | config |
34
+ | `app` | app slug | config |
35
+
36
+ Core types in `opsmith/core/render.py`:
37
+
38
+ ```python
39
+ class InfraBinding(BaseModel):
40
+ host: str; port: int; username: Optional[str]; password: Optional[str]; database: Optional[str]; url: Optional[str]
41
+
42
+ class ServiceBinding(BaseModel):
43
+ host: str; port: Optional[int]
44
+
45
+ class RenderContext(BaseModel):
46
+ infra: Dict[str, InfraBinding]; services: Dict[str, ServiceBinding]; domains: Dict[str, str]
47
+ inputs: Dict[str, str]; env: str; app: str
48
+
49
+ def validate_references(config: DeploymentConfig) -> list[ReferenceError]
50
+ def generated_env_vars(config: DeploymentConfig) -> Dict[str, EnvVarConfig] # env vars whose value calls secret(), by key
51
+ def generate_value(env_var: EnvVarConfig) -> str # its literal text around one fresh secret()
52
+ def secret_keys(config: DeploymentConfig) -> set[str] # env keys holding a secret, declared or derived
53
+ def resolve_env_vars(service: ServiceInfo, ctx: RenderContext, answers: Dict[str, str]) -> Dict[str, str]
54
+ def resolve_files(service: ServiceInfo, ctx: RenderContext) -> Dict[str, str] # container path -> content
55
+ ```
56
+
57
+ Generation is only for credentials opsmith owns: an infra instance's password, and an app secret that the config asks for with `secret()`. Each is generated once, the first time the environment is deployed, and read back from then on. A third-party key is an env var with `is_secret: true` and no `value`. It is always prompted as `envvar.<KEY>` and never generated, because no generated value is ever right for it.
58
+
59
+ Which of the two an env var is gets decided when the config is written, and the deploy only reads the answer. The model decides at detection (2d), a person can change it in the `setup` review, and a harness or a recipe that writes the config decides for itself. The test is whether any random value of the right format would work:
60
+
61
+ - A value the app creates and checks itself gets `secret()`. Examples: Django's `SECRET_KEY`, a JWT signing key, a session or encryption key.
62
+ - A value someone else issues is prompted. Examples: a Stripe key, an OAuth client secret, a webhook secret, an SMTP password, a Sentry DSN.
63
+ - When in doubt it is prompted, because the two mistakes do not cost the same. A generated third-party key deploys and only fails later, at runtime; a prompted app secret costs one question.
64
+
65
+ `secret()` may appear once in an env var's `value`, with nothing around it but literal text: `"{{ secret() }}"`, or `"base64:{{ secret(32, format='base64') }}"` for Laravel's `APP_KEY`. The env var's key names the whole resolved value, and that value is what is stored and read back. That is why `config validate` rejects a `secret()` beside another reference, a second `secret()` in one value, and any `secret()` in `files[].content`, where there is no key to store it under. `format` sets the shape:
66
+
67
+ | `format` | Produces | Example |
68
+ |----------|----------|---------|
69
+ | `alnum` (default) | `length` letters and digits, safe unquoted in URLs, shells and `.env` files | Django `SECRET_KEY`: `secret(50)` |
70
+ | `hex` | `length` random bytes as hex | Rails `SECRET_KEY_BASE`: `secret(64, format='hex')` |
71
+ | `base64` | `length` random bytes as standard base64 | Laravel `APP_KEY`: `base64:` followed by `secret(32, format='base64')` |
72
+ | `urlsafe` | `length` random bytes as URL-safe base64, padded | a Fernet key: `secret(32, format='urlsafe')` |
73
+
74
+ `length` counts characters for `alnum` and random bytes for the other three, because keys in those encodings are specified in bytes. A secret that needs a shape none of these produces is prompted instead.
75
+
76
+ Generation happens before rendering, never in it. The strategy generates every secret that has no persisted value, meaning infra credentials under its binding keys and each env var in `generated_env_vars(config)`, made with `generate_value`, and records each through `ctx.answers` as `envvar.<KEY>` with `secret=True`. A run that stops before the `.env` reaches the machine therefore keeps what it generated, and once the machine's `.env` is fetched back, `adopt_env_file` makes it the source of truth, as it already is for every other secret. The renderer receives all of these in `answers` and only reads them. 2d wires this into `env create` and `update`.
77
+
78
+ A value is a secret when its env var declares `is_secret`, or when its template reads an infra `password`, an infra `url` whose provider's `url_template` carries the password, or `secret()`. `secret_keys(config)` derives that set with the same walk `validate_references` does. It exists because a `DATABASE_URL` built from `{{ infra.postgresql.url }}` holds the password whether or not the config marks it secret.
79
+
80
+ ### Infra provider specs
81
+
82
+ Provider knowledge moves out of prompts and snippets into one registry, `opsmith/infra/providers.py`:
83
+
84
+ ```python
85
+ class InfraProviderSpec(BaseModel):
86
+ provider: InfrastructureProviderEnum
87
+ default_port: int
88
+ url_template: Optional[str] # "postgresql://{username}:{password}@{host}:{port}/{database}"
89
+ default_username: Optional[str] # "{app}" or fixed
90
+ default_database: Optional[str]
91
+ secret_env: Dict[str, str] # legacy env key -> binding attribute, e.g. {"POSTGRES_PASSWORD": "password"}
92
+ default_ram_gb: float # for sizing
93
+ default_cpu: float
94
+ image_for(version, arch) -> str
95
+ settings_keys: list[str] # accepted keys in InfrastructureDependency.settings
96
+ ```
97
+
98
+ Values for the eight providers follow the existing snippets (postgresql 5432, mysql 3306, mongodb 27017, redis 6379, rabbitmq 5672, kafka 9092, elasticsearch 9200, weaviate 8080) with the sizing defaults from the current machine-requirements prompt.
99
+
100
+ `secret_env` records the env keys the current compose snippets already use, because the env file on every deployed VM holds secrets under exactly these names:
101
+
102
+ | Provider | `secret_env` |
103
+ |----------|--------------|
104
+ | postgresql | `POSTGRES_PASSWORD` → password |
105
+ | mysql | `MYSQL_PASSWORD` → password, `MYSQL_ROOT_PASSWORD` → root_password |
106
+ | mongodb | `MONGO_INITDB_ROOT_USERNAME` → username, `MONGO_INITDB_ROOT_PASSWORD` → password |
107
+ | rabbitmq | `RABBITMQ_DEFAULT_USER` → username, `RABBITMQ_DEFAULT_PASS` → password |
108
+ | redis, kafka, elasticsearch, weaviate | none; `password` binds to null and the URL carries no credentials |
109
+
110
+ ### Monolithic bindings
111
+
112
+ ```
113
+ infra.<instance>.host = <instance> (compose service key)
114
+ infra.<instance>.port = spec.default_port
115
+ infra.<instance>.username = settings.username, else the persisted secret for the provider's username key, else spec.default_username
116
+ infra.<instance>.password = persisted secret under the provider's password key from secret_env
117
+ infra.<instance>.database = settings.database or spec.default_database
118
+ infra.<instance>.url = spec.url_template formatted
119
+ services.<slug>.host = <slug>
120
+ services.<slug>.port = service_port
121
+ ```
122
+
123
+ Secret env keys are the legacy names from `secret_env` for the default instance of a provider, the one whose instance name equals the provider value, and `<INSTANCE_UPPER>_<KEY>` for any additional instance, for example `ODOO_DB_POSTGRES_PASSWORD`. A persisted value is always read from the env file fetched from the VM before a new one is generated, so an upgraded environment keeps the credentials its database was initialised with.
124
+
125
+ ### Validation rules
126
+
127
+ Added to the structural rules from [2a](2a-schema-v2-and-upgrade.md), and enforced by `config validate`:
128
+
129
+ - Every `{{ ... }}` reference in `env_vars[].value` and `files[].content` must resolve against the reference grammar above (checked without a strategy).
130
+ - `secret()` may appear once in an env var's `value`, with nothing around it but literal text: never beside another reference, never twice in one value, never in `files[].content`. Its `format` must be `alnum`, `hex`, `base64` or `urlsafe`.
131
+ - An env var key declared by more than one service must have the same `value` in each. The monolithic strategy writes one `.env` that every service reads as `KEY=${KEY}`, so two services whose `DATABASE_URL` points at different instances would otherwise both get whichever was written last.
132
+
133
+ ## Code changes by file
134
+
135
+ | File | Change |
136
+ |------|--------|
137
+ | `opsmith/core/render.py` | new: bindings, reference grammar, `secret()` and its formats, resolvers, secret classification and the secrets to generate |
138
+ | `opsmith/infra/providers.py` | new: the provider spec registry |
139
+ | `opsmith/core/config.py` | the `validate_references` hook, and the `secret()` and duplicate-key rules |
140
+ | `opsmith/deployment_strategies/monolithic.py` | the monolithic bindings, building a `RenderContext` for an environment |
141
+
142
+ ## Acceptance criteria
143
+
144
+ 1. `config validate` rejects a bad reference such as `{{ infra.nope.url }}` with the path to the offending env var, a `secret()` beside another reference, twice in one value or in a file, an unknown `format`, and one env var key declared by two services with different values (phase acceptance criterion 6).
145
+ 2. `secret(32, format='urlsafe')` decodes as URL-safe base64 to 32 bytes, as a Fernet key must, and `"base64:{{ secret(32, format='base64') }}"` has the shape of a Laravel `APP_KEY` (the shapes in phase acceptance criterion 18; 2d proves the rest).
146
+
147
+ ## Tests
148
+
149
+ - `test_render.py`: reference validation, `resolve_env_vars` precedence, `secret()` stability with persisted answers, the `secret()` placement, format and duplicate-key rules, the shape of each format's output, and derived secret keys.
150
+ - The provider registry: every provider has a spec, and each `secret_env` names the keys the v1 snippets use.
151
+
152
+ ## Harness surface
153
+
154
+ None. Nothing a harness writes with references or `secret()` is honoured until 2d, which documents them.
@@ -0,0 +1,91 @@
1
+ # Phase 2c: Compose renderer and preview commands
2
+
3
+ **Goal:** a pure renderer turns the config, an environment's bindings and its answers into a compose file, an env file and mounted files, and three commands preview what it produces, before any deploy uses it.
4
+ **Depends on:** 2b.
5
+ **Size:** L.
6
+ **Lands as:** a part of phase 2; the phase ships as 1.0.0 with phase 3 once 2h lands.
7
+
8
+ ## Scope
9
+
10
+ 1. `ComposeRenderer` and `ComposeBundle`, with golden tests.
11
+ 2. The compose templates: `base.yml`, `services/container.yml.j2`, and infra snippets that take `instance` and `settings`.
12
+ 3. `opsmith compose render`, `compose diff` and `compose validate`.
13
+
14
+ ## Non-goals
15
+
16
+ - Deploying with the renderer ([2d](2d-deterministic-deploys.md)). Until then the LLM compose path deploys, and the previews show what 2d will deploy.
17
+ - `deploy.replicas`, which [2g](2g-capacity-planning.md) adds from the capacity plan.
18
+
19
+ ## Design
20
+
21
+ ### Compose renderer
22
+
23
+ `opsmith/deployment_strategies/compose_renderer.py`:
24
+
25
+ ```python
26
+ class ComposeBundle(BaseModel):
27
+ compose_yaml: str
28
+ env_file: Dict[str, str] # KEY -> value, secrets included
29
+ secret_keys: List[str] # env_file keys holding a secret, from secret_keys(config)
30
+ files: Dict[str, str] # relative path under files/ -> content
31
+ init_jobs: List[Tuple[str, InitJob]] # (service slug, job)
32
+
33
+ class ComposeRenderer:
34
+ def render(self, config, environment, env_state, images: Dict[str, str], answers: Dict[str, str]) -> ComposeBundle
35
+ ```
36
+
37
+ Rules:
38
+
39
+ - Build a dict, not text. Load `base.yml` (traefik, logging anchor, network), add one entry per container service from a single template `services/container.yml.j2`, one per infra instance from the provider spec, then `yaml.safe_dump`.
40
+ - Container service entry: `image` (from `images[slug]` for build sources, `image:tag` for image sources), `command`, `entrypoint`, `environment` as `KEY=${KEY}` references for every resolved env var, `volumes` for named volumes and `./files/<slug>/<n>:<path>:ro` for files, `healthcheck` translated to compose form, `depends_on`, `restart: always`, the logging anchor, and traefik labels per route: router `<slug>-r<i>` with rule ``Host(`<domain>`) && PathPrefix(`<prefix>`)``, service `<slug>-r<i>` pointing at `route.port`, plus the existing https redirect and security-headers middlewares. A service with routes but no domain in the environment is a validation error before rendering.
41
+ - Infra entry: compose key is the instance name; env from provider spec and `settings`; password from the persisted secret; named volume `<instance>-data`.
42
+ - Top-level `volumes:` lists every named volume once.
43
+ - `service_type` no longer affects rendering; it selects Dockerfile templates and, for `STATIC_SITE`, the CDN path only. The per-type snippets stay in place for the LLM path until 2d deletes them.
44
+ - `.env` composition: infra passwords and `secret()` values, resolved `value` templates, then the env vars with no `value`. All of them arrive in `answers`, and the renderer asks nothing and generates nothing: 2d generates the secrets and asks for the rest before rendering.
45
+
46
+ The renderer is pure: same inputs, same bundle. Golden tests cover it.
47
+
48
+ Override files are not merged by the renderer. Phase 3 hands them to Compose, which merges them at deploy time, so the renderer stays pure and overrides survive upgrades.
49
+
50
+ The golden render of an upgraded v1 config is what proves compatibility guarantees 1, 3 and 5, and the key names in guarantee 2, listed in the [README](README.md#compatibility-with-existing-environments).
51
+
52
+ ### Preview commands
53
+
54
+ Three of the commands the [harness spec](../2026-09-04-phase-1-harness-integration.md) lists ship here, because each one needs this phase's renderer and none of them could be written before it:
55
+
56
+ ```
57
+ opsmith compose render --env NAME # rendered docker-compose.yml plus env keys, secret values masked
58
+ opsmith compose diff --env NAME # rendered file against the one currently on the VM
59
+ opsmith compose validate --env NAME # renders, then runs `docker compose config` locally
60
+ ```
61
+
62
+ They also give the first deterministic `update` something to be previewed with. `compose diff` fetches the remote compose file with `_fetch_remote_deployment_files` and prints a unified diff without deploying. Each command returns a typed result, so the generated `commands.md` names its model.
63
+
64
+ `compose render` masks values by `ComposeBundle.secret_keys` rather than by `is_secret` alone, because its output is read by agents. None of the three generates, prompts or writes to the working directory, whose `docker-compose.yml` is what `release` deploys. A secret not yet generated, and an env var not yet answered, are shown as placeholders, so previewing a new environment creates nothing.
65
+
66
+ ## Code changes by file
67
+
68
+ | File | Change |
69
+ |------|--------|
70
+ | `opsmith/deployment_strategies/compose_renderer.py` | new |
71
+ | `opsmith/templates/docker_compose_snippets/` | `services/container.yml.j2`; infra snippets take `instance` and `settings`, which the LLM path passes as the provider until 2d |
72
+ | `opsmith/core/operations.py`, `opsmith/core/results.py` | the three preview operations and their typed results |
73
+ | `opsmith/cli/commands/` | `compose render`, `compose diff` and `compose validate` |
74
+
75
+ ## Acceptance criteria
76
+
77
+ 1. A fixture environment deployed from a v1 config with a captured LLM-style compose file, whose env file holds the legacy secret keys, renders a compose file with identical volume names, infra service keys and secret env keys, and `compose diff` shows only label and formatting changes (phase acceptance criterion 7).
78
+ 2. `compose diff --env dev` prints the difference between the rendered file and the file fetched from the VM without deploying (phase acceptance criterion 8).
79
+ 3. `compose render` masks a `DATABASE_URL` whose value is `{{ infra.postgresql.url }}` although the config does not mark it secret (the masking in phase acceptance criterion 15; 2d proves the rest).
80
+ 4. None of the three commands generates a secret, asks a question or writes to the working directory.
81
+
82
+ ## Tests
83
+
84
+ - `test_compose_renderer.py`: golden files for api+worker+postgres+redis, image-only service with routes and files, two routes on one service; a renderer that never generates.
85
+ - `test_compat_v1_environment.py`: golden render of an upgraded v1 config asserting legacy volume names, infra keys, secret keys and `depends_on`; `compose diff` against a captured LLM-generated file.
86
+ - The preview commands: masking by derived secret keys, placeholders for what is not generated or answered yet, and nothing written.
87
+
88
+ ## Harness surface
89
+
90
+ - `references/commands.md` regenerates for the `compose` commands.
91
+ - `SKILL.md`: the validate loop gains `compose render`, `compose diff` and `compose validate`.