apm-cli 0.12.2__tar.gz → 0.12.4__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 (309) hide show
  1. {apm_cli-0.12.2/src/apm_cli.egg-info → apm_cli-0.12.4}/PKG-INFO +1 -1
  2. {apm_cli-0.12.2 → apm_cli-0.12.4}/pyproject.toml +1 -1
  3. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/claude.py +6 -0
  4. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/copilot.py +481 -39
  5. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/cursor.py +6 -0
  6. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/gemini.py +6 -0
  7. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/opencode.py +6 -0
  8. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/windsurf.py +6 -0
  9. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/bundle/packer.py +2 -2
  10. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/bundle/unpacker.py +2 -2
  11. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/url_normalize.py +6 -3
  12. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cli.py +2 -0
  13. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/_helpers.py +46 -7
  14. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/audit.py +69 -14
  15. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/compile/cli.py +94 -0
  16. apm_cli-0.12.4/src/apm_cli/commands/init.py +572 -0
  17. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/install.py +14 -5
  18. apm_cli-0.12.4/src/apm_cli/commands/targets.py +135 -0
  19. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/link_resolver.py +204 -12
  20. apm_cli-0.12.4/src/apm_cli/core/apm_yml.py +97 -0
  21. apm_cli-0.12.4/src/apm_cli/core/errors.py +156 -0
  22. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/target_detection.py +199 -1
  23. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/plugin_parser.py +10 -12
  24. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/context.py +9 -0
  25. apm_cli-0.12.4/src/apm_cli/install/heals/__init__.py +33 -0
  26. apm_cli-0.12.4/src/apm_cli/install/heals/base.py +122 -0
  27. apm_cli-0.12.4/src/apm_cli/install/heals/branch_ref_drift.py +66 -0
  28. apm_cli-0.12.4/src/apm_cli/install/heals/buggy_lockfile_recovery.py +99 -0
  29. apm_cli-0.12.4/src/apm_cli/install/phases/heal.py +90 -0
  30. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/integrate.py +39 -3
  31. apm_cli-0.12.4/src/apm_cli/install/phases/targets.py +435 -0
  32. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/pipeline.py +17 -2
  33. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/sources.py +59 -16
  34. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/base_integrator.py +9 -0
  35. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/mcp_integrator.py +12 -0
  36. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/skill_integrator.py +8 -12
  37. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/dependency/reference.py +18 -13
  38. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/discovery.py +21 -7
  39. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/outcome_routing.py +17 -8
  40. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/security/__init__.py +2 -0
  41. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/security/gate.py +18 -1
  42. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/content_hash.py +3 -1
  43. {apm_cli-0.12.2 → apm_cli-0.12.4/src/apm_cli.egg-info}/PKG-INFO +1 -1
  44. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli.egg-info/SOURCES.txt +8 -0
  45. apm_cli-0.12.2/src/apm_cli/commands/init.py +0 -294
  46. apm_cli-0.12.2/src/apm_cli/install/phases/targets.py +0 -215
  47. {apm_cli-0.12.2 → apm_cli-0.12.4}/AUTHORS +0 -0
  48. {apm_cli-0.12.2 → apm_cli-0.12.4}/LICENSE +0 -0
  49. {apm_cli-0.12.2 → apm_cli-0.12.4}/NOTICE +0 -0
  50. {apm_cli-0.12.2 → apm_cli-0.12.4}/README.md +0 -0
  51. {apm_cli-0.12.2 → apm_cli-0.12.4}/setup.cfg +0 -0
  52. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/__init__.py +0 -0
  53. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/__init__.py +0 -0
  54. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/__init__.py +0 -0
  55. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/base.py +0 -0
  56. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/codex.py +0 -0
  57. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/client/vscode.py +0 -0
  58. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  59. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/package_manager/base.py +0 -0
  60. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  61. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/bundle/__init__.py +0 -0
  62. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/bundle/local_bundle.py +0 -0
  63. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/bundle/lockfile_enrichment.py +0 -0
  64. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/bundle/plugin_exporter.py +0 -0
  65. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/__init__.py +0 -0
  66. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/git_cache.py +0 -0
  67. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/http_cache.py +0 -0
  68. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/integrity.py +0 -0
  69. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/locking.py +0 -0
  70. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/cache/paths.py +0 -0
  71. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/__init__.py +0 -0
  72. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/_apm_yml_writer.py +0 -0
  73. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/cache.py +0 -0
  74. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/compile/__init__.py +0 -0
  75. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/compile/watcher.py +0 -0
  76. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/config.py +0 -0
  77. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/deps/__init__.py +0 -0
  78. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/deps/_utils.py +0 -0
  79. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/deps/cli.py +0 -0
  80. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/experimental.py +0 -0
  81. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/list_cmd.py +0 -0
  82. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/__init__.py +0 -0
  83. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/check.py +0 -0
  84. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/doctor.py +0 -0
  85. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/init.py +0 -0
  86. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/migrate.py +0 -0
  87. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/outdated.py +0 -0
  88. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/plugin/__init__.py +0 -0
  89. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/plugin/add.py +0 -0
  90. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/plugin/remove.py +0 -0
  91. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/plugin/set.py +0 -0
  92. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/publish.py +0 -0
  93. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/marketplace/validate.py +0 -0
  94. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/mcp.py +0 -0
  95. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/outdated.py +0 -0
  96. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/pack.py +0 -0
  97. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/policy.py +0 -0
  98. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/prune.py +0 -0
  99. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/run.py +0 -0
  100. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/runtime.py +0 -0
  101. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/uninstall/__init__.py +0 -0
  102. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/uninstall/cli.py +0 -0
  103. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/uninstall/engine.py +0 -0
  104. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/update.py +0 -0
  105. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/commands/view.py +0 -0
  106. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/__init__.py +0 -0
  107. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/agents_compiler.py +0 -0
  108. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/build_id.py +0 -0
  109. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/claude_formatter.py +0 -0
  110. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/constants.py +0 -0
  111. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/constitution.py +0 -0
  112. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/constitution_block.py +0 -0
  113. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/context_optimizer.py +0 -0
  114. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/distributed_compiler.py +0 -0
  115. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/gemini_formatter.py +0 -0
  116. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/injector.py +0 -0
  117. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/output_writer.py +0 -0
  118. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/compilation/template_builder.py +0 -0
  119. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/config.py +0 -0
  120. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/constants.py +0 -0
  121. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/__init__.py +0 -0
  122. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/auth.py +0 -0
  123. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/azure_cli.py +0 -0
  124. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/build_orchestrator.py +0 -0
  125. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/command_logger.py +0 -0
  126. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/conflict_detector.py +0 -0
  127. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/docker_args.py +0 -0
  128. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/experimental.py +0 -0
  129. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/null_logger.py +0 -0
  130. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/operations.py +0 -0
  131. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/safe_installer.py +0 -0
  132. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/scope.py +0 -0
  133. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/script_runner.py +0 -0
  134. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/core/token_manager.py +0 -0
  135. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/__init__.py +0 -0
  136. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/aggregator.py +0 -0
  137. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/apm_resolver.py +0 -0
  138. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/artifactory_entry.py +0 -0
  139. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/bare_cache.py +0 -0
  140. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/dependency_graph.py +0 -0
  141. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/download_strategies.py +0 -0
  142. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/git_remote_ops.py +0 -0
  143. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/github_downloader.py +0 -0
  144. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/github_downloader_validation.py +0 -0
  145. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/installed_package.py +0 -0
  146. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/lockfile.py +0 -0
  147. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/package_validator.py +0 -0
  148. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/registry_proxy.py +0 -0
  149. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/shared_clone_cache.py +0 -0
  150. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/transport_selection.py +0 -0
  151. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/deps/verifier.py +0 -0
  152. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/drift.py +0 -0
  153. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/factory.py +0 -0
  154. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/__init__.py +0 -0
  155. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/cache_pin.py +0 -0
  156. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/drift.py +0 -0
  157. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/errors.py +0 -0
  158. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/helpers/__init__.py +0 -0
  159. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/helpers/security_scan.py +0 -0
  160. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/insecure_policy.py +0 -0
  161. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/local_bundle_handler.py +0 -0
  162. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/__init__.py +0 -0
  163. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/args.py +0 -0
  164. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/command.py +0 -0
  165. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/conflicts.py +0 -0
  166. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/entry.py +0 -0
  167. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/registry.py +0 -0
  168. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/warnings.py +0 -0
  169. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/mcp/writer.py +0 -0
  170. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/__init__.py +0 -0
  171. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/cleanup.py +0 -0
  172. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/download.py +0 -0
  173. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/finalize.py +0 -0
  174. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/local_content.py +0 -0
  175. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/lockfile.py +0 -0
  176. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/policy_gate.py +0 -0
  177. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/policy_target_check.py +0 -0
  178. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/post_deps_local.py +0 -0
  179. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/phases/resolve.py +0 -0
  180. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/presentation/__init__.py +0 -0
  181. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/presentation/dry_run.py +0 -0
  182. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/request.py +0 -0
  183. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/service.py +0 -0
  184. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/services.py +0 -0
  185. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/skill_path_migration.py +0 -0
  186. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/summary.py +0 -0
  187. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/template.py +0 -0
  188. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/install/validation.py +0 -0
  189. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/__init__.py +0 -0
  190. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/agent_integrator.py +0 -0
  191. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/cleanup.py +0 -0
  192. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/command_integrator.py +0 -0
  193. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/copilot_cowork_paths.py +0 -0
  194. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/coverage.py +0 -0
  195. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/dispatch.py +0 -0
  196. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/hook_integrator.py +0 -0
  197. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/instruction_integrator.py +0 -0
  198. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/prompt_integrator.py +0 -0
  199. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/skill_transformer.py +0 -0
  200. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/targets.py +0 -0
  201. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/integration/utils.py +0 -0
  202. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/__init__.py +0 -0
  203. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/_git_utils.py +0 -0
  204. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/_io.py +0 -0
  205. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/builder.py +0 -0
  206. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/client.py +0 -0
  207. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/errors.py +0 -0
  208. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/git_stderr.py +0 -0
  209. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/init_template.py +0 -0
  210. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/migration.py +0 -0
  211. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/models.py +0 -0
  212. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/pr_integration.py +0 -0
  213. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/publisher.py +0 -0
  214. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/ref_resolver.py +0 -0
  215. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/registry.py +0 -0
  216. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/resolver.py +0 -0
  217. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/semver.py +0 -0
  218. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/shadow_detector.py +0 -0
  219. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/tag_pattern.py +0 -0
  220. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/validator.py +0 -0
  221. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/version_pins.py +0 -0
  222. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/yml_editor.py +0 -0
  223. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/marketplace/yml_schema.py +0 -0
  224. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/__init__.py +0 -0
  225. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/apm_package.py +0 -0
  226. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/dependency/__init__.py +0 -0
  227. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/dependency/mcp.py +0 -0
  228. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/dependency/types.py +0 -0
  229. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/plugin.py +0 -0
  230. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/results.py +0 -0
  231. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/models/validation.py +0 -0
  232. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/output/__init__.py +0 -0
  233. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/output/formatters.py +0 -0
  234. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/output/models.py +0 -0
  235. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/output/script_formatters.py +0 -0
  236. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/__init__.py +0 -0
  237. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/_help_text.py +0 -0
  238. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/ci_checks.py +0 -0
  239. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/inheritance.py +0 -0
  240. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/install_preflight.py +0 -0
  241. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/matcher.py +0 -0
  242. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/models.py +0 -0
  243. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/parser.py +0 -0
  244. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/policy_checks.py +0 -0
  245. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/project_config.py +0 -0
  246. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/policy/schema.py +0 -0
  247. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/primitives/__init__.py +0 -0
  248. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/primitives/discovery.py +0 -0
  249. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/primitives/models.py +0 -0
  250. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/primitives/parser.py +0 -0
  251. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/registry/__init__.py +0 -0
  252. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/registry/client.py +0 -0
  253. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/registry/integration.py +0 -0
  254. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/registry/operations.py +0 -0
  255. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/__init__.py +0 -0
  256. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/base.py +0 -0
  257. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/codex_runtime.py +0 -0
  258. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  259. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/factory.py +0 -0
  260. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/llm_runtime.py +0 -0
  261. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/runtime/manager.py +0 -0
  262. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/security/audit_report.py +0 -0
  263. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/security/content_scanner.py +0 -0
  264. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/security/file_scanner.py +0 -0
  265. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/update_policy.py +0 -0
  266. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/__init__.py +0 -0
  267. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/atomic_io.py +0 -0
  268. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/console.py +0 -0
  269. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/diagnostics.py +0 -0
  270. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/exclude.py +0 -0
  271. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/file_ops.py +0 -0
  272. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/git_env.py +0 -0
  273. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/github_host.py +0 -0
  274. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/guards.py +0 -0
  275. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/helpers.py +0 -0
  276. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/install_tui.py +0 -0
  277. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/normalization.py +0 -0
  278. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/path_security.py +0 -0
  279. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/paths.py +0 -0
  280. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/reflink.py +0 -0
  281. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/short_sha.py +0 -0
  282. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/subprocess_env.py +0 -0
  283. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/version_checker.py +0 -0
  284. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/utils/yaml_io.py +0 -0
  285. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/version.py +0 -0
  286. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/workflow/__init__.py +0 -0
  287. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/workflow/discovery.py +0 -0
  288. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/workflow/parser.py +0 -0
  289. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli/workflow/runner.py +0 -0
  290. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  291. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli.egg-info/entry_points.txt +0 -0
  292. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli.egg-info/requires.txt +0 -0
  293. {apm_cli-0.12.2 → apm_cli-0.12.4}/src/apm_cli.egg-info/top_level.txt +0 -0
  294. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_apm_package_models.py +0 -0
  295. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_apm_resolver.py +0 -0
  296. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_codex_docker_args_fix.py +0 -0
  297. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_codex_empty_string_and_defaults.py +0 -0
  298. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_collision_integration.py +0 -0
  299. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_console.py +0 -0
  300. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_distributed_compilation.py +0 -0
  301. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_empty_string_and_defaults.py +0 -0
  302. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_enhanced_discovery.py +0 -0
  303. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_github_downloader.py +0 -0
  304. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_github_downloader_token_precedence.py +0 -0
  305. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_lockfile.py +0 -0
  306. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_runnable_prompts.py +0 -0
  307. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_runtime_manager_token_precedence.py +0 -0
  308. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_token_manager.py +0 -0
  309. {apm_cli-0.12.2 → apm_cli-0.12.4}/tests/test_virtual_package_multi_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apm-cli
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: MCP configuration tool
5
5
  Author-email: Daniel Meppiel <user@example.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.12.2"
7
+ version = "0.12.4"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -42,6 +42,12 @@ class ClaudeClientAdapter(CopilotClientAdapter):
42
42
  target_name: str = "claude"
43
43
  mcp_servers_key: str = "mcpServers"
44
44
 
45
+ # Claude Desktop / Code's mcp config does NOT support runtime env-var
46
+ # substitution -- the value in ``env`` must be a literal string. This
47
+ # adapter MUST keep the legacy install-time resolution behaviour.
48
+ # See #1152 supply-chain analysis.
49
+ _supports_runtime_env_substitution: bool = False
50
+
45
51
  @staticmethod
46
52
  def _normalize_mcp_entry_for_claude_code(entry: dict) -> dict:
47
53
  """Normalize a server entry to Claude Code's on-disk shape.
@@ -9,11 +9,15 @@ import json
9
9
  import os
10
10
  import re
11
11
  from pathlib import Path
12
+ from typing import ClassVar
13
+
14
+ import click
12
15
 
13
16
  from ...core.docker_args import DockerArgsProcessor
14
17
  from ...core.token_manager import GitHubTokenManager
15
18
  from ...registry.client import SimpleRegistryClient
16
19
  from ...registry.integration import RegistryIntegration
20
+ from ...utils.console import _rich_warning
17
21
  from ...utils.github_host import is_github_hostname
18
22
  from .base import _ENV_VAR_RE, MCPClientAdapter
19
23
 
@@ -27,6 +31,67 @@ from .base import _ENV_VAR_RE, MCPClientAdapter
27
31
  # per-call cost. ``${input:...}`` is intentionally not matched here.
28
32
  _COPILOT_ENV_RE = re.compile(r"<([A-Z_][A-Z0-9_]*)>|" + _ENV_VAR_RE.pattern)
29
33
 
34
+ # Detects the legacy ``<VAR>`` placeholder syntax. Used both for translation
35
+ # and for emitting an aggregated deprecation warning, mirroring the analogous
36
+ # pattern in ``vscode.py``.
37
+ _LEGACY_ANGLE_VAR_RE = re.compile(r"<([A-Z_][A-Z0-9_]*)>")
38
+
39
+
40
+ def _translate_env_placeholder(value):
41
+ """Pure-textual translation of env-var placeholders to Copilot CLI's
42
+ native runtime substitution syntax (``${VAR}``).
43
+
44
+ This is the security-critical helper for issue #1152: it MUST NOT read
45
+ ``os.environ`` and MUST NOT resolve placeholders to their literal values.
46
+ Copilot CLI resolves ``${VAR}`` from the host environment at server-start
47
+ time, so APM emits placeholders verbatim rather than baking secrets into
48
+ ``~/.copilot/mcp-config.json``.
49
+
50
+ Translations:
51
+ ``${env:VAR}`` -> ``${VAR}`` (strip ``env:`` prefix)
52
+ ``${VAR}`` -> ``${VAR}`` (no-op)
53
+ ``<VAR>`` -> ``${VAR}`` (legacy syntax migration)
54
+ ``${VAR:-default}``-> passthrough (regex doesn't match)
55
+ ``$VAR`` (bare) -> passthrough (regex doesn't match)
56
+ ``${input:foo}`` -> passthrough (regex doesn't match)
57
+ non-string -> passthrough
58
+
59
+ The translation is idempotent: applying it twice produces the same
60
+ result as applying it once.
61
+ """
62
+ if not isinstance(value, str):
63
+ return value
64
+
65
+ def _to_brace(match):
66
+ # group(1) = legacy <VAR>; group(2) = ${VAR} / ${env:VAR}
67
+ var_name = match.group(1) or match.group(2)
68
+ return "${" + var_name + "}"
69
+
70
+ return _COPILOT_ENV_RE.sub(_to_brace, value)
71
+
72
+
73
+ def _extract_legacy_angle_vars(value):
74
+ """Return the set of legacy ``<VAR>`` names present in *value*.
75
+
76
+ Used to aggregate deprecation warnings across all servers in a single
77
+ install run, so authors see one helpful list instead of one warning per
78
+ occurrence.
79
+ """
80
+ if not isinstance(value, str):
81
+ return set()
82
+ return set(_LEGACY_ANGLE_VAR_RE.findall(value))
83
+
84
+
85
+ def _has_env_placeholder(value):
86
+ """True if *value* is a string containing any recognised env-var
87
+ placeholder syntax (``${VAR}``, ``${env:VAR}``, or legacy ``<VAR>``).
88
+ Used to distinguish placeholder-sourced env values (which translate)
89
+ from hardcoded literal defaults (which stay literal).
90
+ """
91
+ if not isinstance(value, str):
92
+ return False
93
+ return bool(_COPILOT_ENV_RE.search(value))
94
+
30
95
 
31
96
  class CopilotClientAdapter(MCPClientAdapter):
32
97
  """Copilot CLI implementation of MCP client adapter.
@@ -41,6 +106,42 @@ class CopilotClientAdapter(MCPClientAdapter):
41
106
  target_name: str = "copilot"
42
107
  mcp_servers_key: str = "mcpServers"
43
108
 
109
+ # When True, env-var placeholders (``${VAR}``, ``${env:VAR}``, legacy
110
+ # ``<VAR>``) are translated to Copilot CLI's native runtime-substitution
111
+ # syntax (``${VAR}``) and emitted into mcp-config.json verbatim. The
112
+ # secret never touches disk.
113
+ #
114
+ # When False, placeholders are resolved at install time against the host
115
+ # environment and the literal value is baked into the config file
116
+ # (legacy pre-#1152 behaviour).
117
+ #
118
+ # Subclasses (Cursor / Windsurf / OpenCode / Claude / Gemini) override
119
+ # this to ``False`` until their respective config formats are individually
120
+ # audited for runtime-substitution support. Critically, Claude Desktop's
121
+ # config format does NOT support runtime substitution -- it MUST keep
122
+ # resolving at install time.
123
+ _supports_runtime_env_substitution: bool = True
124
+
125
+ # Process-wide aggregation of legacy ``<VAR>`` offenders, keyed by
126
+ # adapter class so subclasses (Cursor, etc.) maintain their own
127
+ # buckets. Populated by ``configure_mcp_server`` and drained by the
128
+ # post-install summary helper. Class-level so cross-server warnings
129
+ # work even when a fresh adapter instance is created per dep.
130
+ _legacy_angle_offenders_by_server: ClassVar[dict] = {}
131
+ # Process-wide aggregation of env-var keys whose values were previously
132
+ # baked as plaintext literals on disk and have just been rewritten to
133
+ # ``${KEY}`` placeholders. Drives the security-improvement notice.
134
+ _security_upgraded_keys: ClassVar[set] = set()
135
+ # Process-wide aggregation of env-var names referenced by configs that
136
+ # are NOT exported in the current shell. Drives the post-install
137
+ # actionable warning that lists vars the user must export before
138
+ # launching ``gh copilot``.
139
+ _unset_env_keys_by_server: ClassVar[dict] = {}
140
+ # Guard so the post-install summary is emitted at most once per CLI
141
+ # invocation, regardless of how many ``configure_mcp_server`` calls
142
+ # contributed to the aggregation buckets.
143
+ _install_run_summary_emitted: ClassVar[bool] = False
144
+
44
145
  def __init__(
45
146
  self,
46
147
  registry_url=None,
@@ -61,6 +162,14 @@ class CopilotClientAdapter(MCPClientAdapter):
61
162
  super().__init__(project_root=project_root, user_scope=user_scope)
62
163
  self.registry_client = SimpleRegistryClient(registry_url)
63
164
  self.registry_integration = RegistryIntegration(registry_url)
165
+ # Per-server tracking of placeholder-sourced env-var keys, populated
166
+ # during ``_format_server_config`` and consumed by the post-install
167
+ # summary line. Keys: env-var names; never holds resolved values.
168
+ self._last_env_placeholder_keys = set()
169
+ # Per-server collection of legacy ``<VAR>`` offenders, populated by
170
+ # the resolution helpers and consumed by ``configure_mcp_server`` to
171
+ # feed the aggregated deprecation warning.
172
+ self._last_legacy_angle_vars = set()
64
173
 
65
174
  def get_config_path(self):
66
175
  """Get the path to the Copilot CLI MCP configuration file.
@@ -154,6 +263,26 @@ class CopilotClientAdapter(MCPClientAdapter):
154
263
  print(f"Error: MCP server '{server_url}' not found in registry")
155
264
  return False
156
265
 
266
+ # Reset per-server tracking before formatting (so the per-server
267
+ # summary line and aggregated diagnostics reflect this server only).
268
+ self._last_env_placeholder_keys = set()
269
+ self._last_legacy_angle_vars = set()
270
+
271
+ # Detect security upgrade: was the previous on-disk config for
272
+ # this server holding literal (resolved) values for env keys
273
+ # we are about to replace with ${KEY} placeholders? If so,
274
+ # remember the affected keys for the post-install notice. We
275
+ # snapshot BEFORE writing the new config.
276
+ previously_baked_keys = set()
277
+ previously_baked_headers = False
278
+ if self._supports_runtime_env_substitution:
279
+ previously_baked_keys, previously_baked_headers = (
280
+ self._collect_previously_baked_keys(server_url, server_name)
281
+ )
282
+
283
+ # Generate server configuration with environment and runtime variable resolution
284
+ server_config = self._format_server_config(server_info, env_overrides, runtime_vars)
285
+
157
286
  # Determine the server name for configuration key
158
287
  if server_name:
159
288
  # Use explicitly provided server name
@@ -168,19 +297,193 @@ class CopilotClientAdapter(MCPClientAdapter):
168
297
  # Fallback to full server_url if no slash
169
298
  config_key = server_url
170
299
 
171
- # Generate server configuration with environment and runtime variable resolution
172
- server_config = self._format_server_config(server_info, env_overrides, runtime_vars)
173
-
174
300
  # Update configuration using the chosen key
175
301
  self.update_config({config_key: server_config})
176
302
 
177
- print(f"Successfully configured MCP server '{config_key}' for {self._client_label}")
303
+ # Aggregate diagnostics for the post-install summary.
304
+ if self._supports_runtime_env_substitution:
305
+ if self._last_legacy_angle_vars:
306
+ self._legacy_angle_offenders_by_server[config_key] = set(
307
+ self._last_legacy_angle_vars
308
+ )
309
+ # Only flag a security upgrade when the previously baked keys
310
+ # actually overlap with what we are now placeholderizing -- OR
311
+ # when the previous on-disk state had baked HTTP header
312
+ # literals (which don't expose env-var names directly, so we
313
+ # surface every newly-placeholderised key for this server).
314
+ upgraded = previously_baked_keys & self._last_env_placeholder_keys
315
+ if previously_baked_headers and self._last_env_placeholder_keys:
316
+ upgraded = upgraded | self._last_env_placeholder_keys
317
+ if upgraded:
318
+ self._security_upgraded_keys.update(upgraded)
319
+
320
+ # Per-server install line with env-var summary parenthetical.
321
+ self._emit_install_summary(config_key, server_config)
178
322
  return True
179
323
 
180
324
  except Exception as e:
181
325
  print(f"Error configuring MCP server: {e}")
182
326
  return False
183
327
 
328
+ def _collect_previously_baked_keys(self, server_url, server_name):
329
+ """Return ``(env_keys, headers_were_baked)`` for the existing on-disk
330
+ entry: the set of env-block keys whose values are literal
331
+ (non-placeholder) strings, and a flag indicating whether the headers
332
+ block contained any literal values. Together these drive the
333
+ security-improvement notice. Headers don't expose env-var names
334
+ directly, so the caller unions current-write placeholder keys when
335
+ ``headers_were_baked`` is True.
336
+ """
337
+ try:
338
+ current = self.get_current_config()
339
+ except Exception:
340
+ return set(), False
341
+ servers = current.get("mcpServers") or {}
342
+ # Match the same key resolution rule used below.
343
+ if server_name:
344
+ key = server_name
345
+ elif "/" in server_url:
346
+ key = server_url.split("/")[-1]
347
+ else:
348
+ key = server_url
349
+ existing = servers.get(key)
350
+ if not isinstance(existing, dict):
351
+ return set(), False
352
+ baked_env_keys = set()
353
+ env_block = existing.get("env") or {}
354
+ if isinstance(env_block, dict):
355
+ for k, v in env_block.items():
356
+ if isinstance(v, str) and v.strip() and not _has_env_placeholder(v):
357
+ baked_env_keys.add(k)
358
+ headers_were_baked = False
359
+ headers_block = existing.get("headers") or {}
360
+ if isinstance(headers_block, dict):
361
+ for v in headers_block.values():
362
+ if isinstance(v, str) and v.strip() and not _has_env_placeholder(v):
363
+ headers_were_baked = True
364
+ break
365
+ return baked_env_keys, headers_were_baked
366
+
367
+ def _emit_install_summary(self, config_key, server_config):
368
+ """Record env-var references for the post-install aggregated
369
+ summary. No per-server line is emitted here; the integrator's
370
+ tree (``| + {name} -> Copilot (configured)``) is the success
371
+ signal. The summary references env-var names only -- never their
372
+ values.
373
+ """
374
+ if not self._supports_runtime_env_substitution:
375
+ return
376
+ keys = set(self._last_env_placeholder_keys)
377
+ if isinstance(server_config, dict):
378
+ for block_key in ("env", "headers"):
379
+ block = server_config.get(block_key)
380
+ if not isinstance(block, dict):
381
+ continue
382
+ for value in block.values():
383
+ if isinstance(value, str):
384
+ for match in _ENV_VAR_RE.finditer(value):
385
+ keys.add(match.group(1))
386
+ unset = sorted(name for name in keys if not os.environ.get(name))
387
+ if unset:
388
+ self.__class__._unset_env_keys_by_server.setdefault(config_key, []).extend(
389
+ u
390
+ for u in unset
391
+ if u not in self.__class__._unset_env_keys_by_server.get(config_key, [])
392
+ )
393
+
394
+ @classmethod
395
+ def emit_install_run_summary(cls):
396
+ """Emit aggregated cross-server diagnostics at the end of an install
397
+ run. Idempotent: subsequent calls within the same process are no-ops.
398
+
399
+ Three diagnostics are emitted (when applicable):
400
+
401
+ 1. Security improvement notice -- when the install rewrote
402
+ previously baked literal env values to runtime placeholders.
403
+ Emitted as a warning because it is an action item (the user
404
+ must export the affected vars).
405
+ 2. Aggregated unset-env warning -- when one or more configured
406
+ servers reference env vars that are not currently exported.
407
+ Includes a copy-pasteable ``export`` hint.
408
+ 3. Aggregated legacy ``<VAR>`` deprecation warning -- one line
409
+ naming all affected servers, mirroring the established VS Code
410
+ adapter pattern.
411
+
412
+ State is drained after emission so a subsequent install run in
413
+ the same process (e.g. tests) starts clean.
414
+ """
415
+ if cls._install_run_summary_emitted:
416
+ return
417
+
418
+ # Visual separator from the install tree's closing line so the
419
+ # post-tree summary block reads as a distinct section.
420
+ emitted_any = False
421
+
422
+ def _emit_separator_once():
423
+ nonlocal emitted_any
424
+ if not emitted_any:
425
+ click.echo("")
426
+ emitted_any = True
427
+
428
+ if cls._security_upgraded_keys:
429
+ visible = sorted(cls._security_upgraded_keys)
430
+ count = len(visible)
431
+ noun = "variable" if count == 1 else "variables"
432
+ affected = ", ".join(visible)
433
+ _emit_separator_once()
434
+ _rich_warning(
435
+ f"Security improvement: {count} environment {noun} previously stored as "
436
+ f"plaintext in the Copilot config are now resolved at runtime.\n"
437
+ f" Affected: {affected}\n"
438
+ f" Ensure these are exported in your shell before running 'gh copilot'",
439
+ symbol="warning",
440
+ )
441
+ if cls._unset_env_keys_by_server:
442
+ all_unset: set[str] = set()
443
+ for names in cls._unset_env_keys_by_server.values():
444
+ all_unset.update(names)
445
+ sorted_unset = sorted(all_unset)
446
+ export_hint = " ".join(f"{name}=..." for name in sorted_unset)
447
+ count = len(sorted_unset)
448
+ noun = "variable" if count == 1 else "variables"
449
+ _emit_separator_once()
450
+ _rich_warning(
451
+ f"Copilot CLI will resolve {count} environment {noun} at runtime "
452
+ f"that {'is' if count == 1 else 'are'} not currently set: "
453
+ f"{', '.join(sorted_unset)}.\n"
454
+ f" Export {'it' if count == 1 else 'them'} in your shell before "
455
+ f"running 'gh copilot', e.g.:\n"
456
+ f" export {export_hint}",
457
+ symbol="warning",
458
+ )
459
+ # Deprecation notice is informational housekeeping (not a runtime
460
+ # blocker), but it ships unguarded for now so legacy <VAR> usage
461
+ # remains visible until the v1.0 removal. If --quiet gating is
462
+ # added in future, the unset-env and security warnings above must
463
+ # remain unsuppressible because they describe action-required state.
464
+ if cls._legacy_angle_offenders_by_server:
465
+ servers = sorted(cls._legacy_angle_offenders_by_server.keys())
466
+ count = len(servers)
467
+ noun = "server" if count == 1 else "servers"
468
+ _emit_separator_once()
469
+ _rich_warning(
470
+ f"Deprecated: <VAR> placeholder syntax used in {count} {noun} "
471
+ f"({', '.join(servers)}). Migrate to ${{VAR}} in apm.yml. "
472
+ f"<VAR> support will be removed in v1.0.",
473
+ symbol="warning",
474
+ )
475
+ cls._install_run_summary_emitted = True
476
+
477
+ @classmethod
478
+ def reset_install_run_state(cls):
479
+ """Reset the process-wide aggregation buckets. Intended for tests
480
+ and for explicitly starting a new install run within the same
481
+ process."""
482
+ cls._legacy_angle_offenders_by_server = {}
483
+ cls._security_upgraded_keys = set()
484
+ cls._unset_env_keys_by_server = {}
485
+ cls._install_run_summary_emitted = False
486
+
184
487
  def _format_server_config(self, server_info, env_overrides=None, runtime_vars=None):
185
488
  """Format server information into Copilot CLI MCP configuration format.
186
489
 
@@ -202,14 +505,27 @@ class CopilotClientAdapter(MCPClientAdapter):
202
505
  "id": server_info.get("id", ""), # Add registry UUID for conflict detection
203
506
  }
204
507
 
205
- # Self-defined stdio deps carry raw command/args -- use directly
508
+ # Self-defined stdio deps carry raw command/args -- use directly,
509
+ # but route values through the env-var translation/resolution pipeline
510
+ # so secrets are not baked into the persisted config when the harness
511
+ # supports runtime substitution (Copilot CLI).
206
512
  raw = server_info.get("_raw_stdio")
207
513
  if raw:
208
514
  config["command"] = raw["command"]
209
- config["args"] = raw["args"]
515
+ resolved_env_for_args = {}
210
516
  if raw.get("env"):
211
- config["env"] = raw["env"]
517
+ resolved_env_for_args = self._resolve_environment_variables(
518
+ raw["env"], env_overrides=env_overrides
519
+ )
520
+ config["env"] = resolved_env_for_args
212
521
  self._warn_input_variables(raw["env"], server_info.get("name", ""), "Copilot CLI")
522
+ args = raw.get("args") or []
523
+ config["args"] = [
524
+ self._resolve_variable_placeholders(arg, resolved_env_for_args, runtime_vars)
525
+ if isinstance(arg, str)
526
+ else arg
527
+ for arg in args
528
+ ]
213
529
  # Apply tools override if present
214
530
  tools_override = server_info.get("_apm_tools_override")
215
531
  if tools_override:
@@ -385,15 +701,93 @@ class CopilotClientAdapter(MCPClientAdapter):
385
701
  return config
386
702
 
387
703
  def _resolve_environment_variables(self, env_vars, env_overrides=None):
388
- """Resolve environment variables to actual values.
704
+ """Resolve (or translate) declared environment variables.
705
+
706
+ Behaviour depends on ``self._supports_runtime_env_substitution``:
707
+
708
+ - True (Copilot CLI default): each declared env var ``NAME`` gets a
709
+ ``${NAME}`` placeholder that Copilot CLI resolves at server-start
710
+ from the host environment. Hardcoded literal defaults
711
+ (``GITHUB_TOOLSETS``, ``GITHUB_DYNAMIC_TOOLSETS``) stay literal
712
+ because they are not secrets and provide essential server
713
+ configuration. The host environment is NOT read; secrets never
714
+ touch disk. See issue #1152 for context.
715
+
716
+ - False (legacy / sibling-adapter behaviour): resolve each variable
717
+ to its literal value via ``env_overrides`` -> ``os.environ`` ->
718
+ optional interactive prompt, baking the result into the config.
389
719
 
390
720
  Args:
391
- env_vars (list): List of environment variable definitions from server info.
392
- env_overrides (dict, optional): Pre-collected environment variable overrides.
721
+ env_vars (list): List of environment variable definitions from
722
+ server info (each item is ``{name, description, required}``).
723
+ env_overrides (dict, optional): Pre-collected environment
724
+ variable overrides. Ignored in translate mode.
393
725
 
394
726
  Returns:
395
- dict: Dictionary of resolved environment variables.
727
+ dict: ``{name: value}`` -- placeholder string in translate mode,
728
+ literal value in legacy mode.
396
729
  """
730
+ # Hardcoded literal defaults that supply essential server behaviour
731
+ # rather than secrets. These stay literal in translate mode so that
732
+ # tool-selection still works without a user export step.
733
+ default_github_env = {"GITHUB_TOOLSETS": "context", "GITHUB_DYNAMIC_TOOLSETS": "1"}
734
+
735
+ # Self-defined stdio deps pass ``env`` as a plain dict
736
+ # ({NAME: value-or-placeholder}); registry-sourced deps pass a list
737
+ # of {name, description, required} dicts. Translate-mode handling
738
+ # for the dict shape: each value is either already a placeholder
739
+ # (translate it to the canonical ${VAR} form) or a literal (record
740
+ # the key as a placeholder reference and emit ${NAME} so the
741
+ # value never lands on disk). See issue #1152.
742
+ if isinstance(env_vars, dict) and self._supports_runtime_env_substitution:
743
+ translated = {}
744
+ placeholder_keys = []
745
+ for name, raw_value in env_vars.items():
746
+ if not name:
747
+ continue
748
+ if not isinstance(raw_value, str):
749
+ translated[name] = raw_value
750
+ continue
751
+ if _has_env_placeholder(raw_value):
752
+ self._last_legacy_angle_vars.update(_extract_legacy_angle_vars(raw_value))
753
+ translated[name] = _translate_env_placeholder(raw_value)
754
+ # Record every ${VAR} in the translated value (handles
755
+ # both ${env:VAR} -> ${VAR} and bare ${VAR} cases).
756
+ for match in _ENV_VAR_RE.finditer(translated[name]):
757
+ placeholder_keys.append(match.group(1))
758
+ elif name in default_github_env and raw_value == default_github_env[name]:
759
+ translated[name] = raw_value
760
+ else:
761
+ # Literal value present in apm.yml -- replace with a
762
+ # runtime placeholder so the secret never touches disk.
763
+ translated[name] = "${" + name + "}"
764
+ placeholder_keys.append(name)
765
+ self._last_env_placeholder_keys = set(placeholder_keys)
766
+ return translated
767
+
768
+ if self._supports_runtime_env_substitution:
769
+ resolved = {}
770
+ placeholder_keys = []
771
+ for env_var in env_vars:
772
+ if not isinstance(env_var, dict):
773
+ continue
774
+ name = env_var.get("name", "")
775
+ if not name:
776
+ continue
777
+ if name in default_github_env:
778
+ # Non-secret literal default -- preserve as-is.
779
+ resolved[name] = default_github_env[name]
780
+ else:
781
+ # Emit a runtime-substitution placeholder; Copilot CLI
782
+ # resolves ``${NAME}`` from the host environment at
783
+ # server-start. APM never reads or stores the value.
784
+ resolved[name] = "${" + name + "}"
785
+ placeholder_keys.append(name)
786
+ # Record for the post-install summary line and the
787
+ # security-improvement notice.
788
+ self._last_env_placeholder_keys = set(placeholder_keys)
789
+ return resolved
790
+
397
791
  import os
398
792
  import sys
399
793
 
@@ -416,10 +810,6 @@ class CopilotClientAdapter(MCPClientAdapter):
416
810
  if not is_interactive:
417
811
  skip_prompting = True
418
812
 
419
- # Add default GitHub MCP server environment variables for essential functionality first
420
- # This ensures variables have defaults when user provides empty values or they're optional
421
- default_github_env = {"GITHUB_TOOLSETS": "context", "GITHUB_DYNAMIC_TOOLSETS": "1"}
422
-
423
813
  # Track which variables were explicitly provided with empty values (user wants defaults)
424
814
  empty_value_vars = set()
425
815
  if env_overrides:
@@ -465,16 +855,42 @@ class CopilotClientAdapter(MCPClientAdapter):
465
855
  return resolved
466
856
 
467
857
  def _resolve_env_variable(self, name, value, env_overrides=None):
468
- """Resolve a single environment variable value.
858
+ """Resolve (or translate) a single environment variable value.
859
+
860
+ Behaviour depends on ``self._supports_runtime_env_substitution``:
861
+
862
+ - True (Copilot CLI default): translate placeholders to Copilot CLI's
863
+ native runtime substitution syntax (``${VAR}``). The host
864
+ environment is NOT read; the secret never touches disk. See issue
865
+ #1152 for context. Legacy ``<VAR>`` offenders are tracked for the
866
+ aggregated deprecation warning emitted by
867
+ ``configure_mcp_server``.
868
+
869
+ - False (legacy / sibling-adapter behaviour): resolve placeholders
870
+ to literal values via ``env_overrides`` -> ``os.environ`` ->
871
+ optional interactive prompt, baking the result into the config.
469
872
 
470
873
  Args:
471
874
  name (str): Environment variable name.
472
875
  value (str): Environment variable value or placeholder.
473
- env_overrides (dict, optional): Pre-collected environment variable overrides.
876
+ env_overrides (dict, optional): Pre-collected environment
877
+ variable overrides. Ignored in translate mode.
474
878
 
475
879
  Returns:
476
- str: Resolved environment variable value.
880
+ str: Translated placeholder (translate mode) or resolved
881
+ literal value (legacy mode).
477
882
  """
883
+ if self._supports_runtime_env_substitution:
884
+ # Track legacy <VAR> offenders for the aggregated deprecation
885
+ # warning. Translation itself is a pure-textual rewrite.
886
+ self._last_legacy_angle_vars.update(_extract_legacy_angle_vars(value))
887
+ # Track env-var names referenced via this header/value so the
888
+ # security-upgrade detector and per-server summary can see
889
+ # them (the env-block path tracks via _resolve_environment_variables).
890
+ for match in _ENV_VAR_RE.finditer(value):
891
+ self._last_env_placeholder_keys.add(match.group(1))
892
+ return _translate_env_placeholder(value)
893
+
478
894
  import sys
479
895
 
480
896
  from rich.prompt import Prompt
@@ -683,15 +1099,31 @@ class CopilotClientAdapter(MCPClientAdapter):
683
1099
  return processed
684
1100
 
685
1101
  def _resolve_variable_placeholders(self, value, resolved_env, runtime_vars):
686
- """Resolve both environment and runtime variable placeholders in values.
1102
+ """Resolve runtime template variables and translate or resolve env-var
1103
+ placeholders in argument strings.
1104
+
1105
+ Behaviour depends on ``self._supports_runtime_env_substitution``:
1106
+
1107
+ - True (Copilot CLI default): env-var placeholders (``<VAR>``,
1108
+ ``${VAR}``, ``${env:VAR}``) are translated to ``${VAR}`` for
1109
+ runtime substitution by Copilot CLI. APM template variables
1110
+ (``{runtime_var}``) are still resolved at install time because
1111
+ they are an APM-internal concept Copilot cannot interpret.
1112
+
1113
+ - False (legacy / sibling-adapter behaviour): legacy ``<VAR>``
1114
+ placeholders are resolved against ``resolved_env`` (the dict of
1115
+ literal env-var values), and ``{runtime_var}`` against
1116
+ ``runtime_vars``. Newer ``${VAR}`` / ``${env:VAR}`` syntaxes are
1117
+ left as-is for backward compatibility.
687
1118
 
688
1119
  Args:
689
- value (str): Value that may contain placeholders like <TOKEN_NAME> or {ado_org}
690
- resolved_env (dict): Dictionary of resolved environment variables.
1120
+ value (str): Value that may contain placeholders.
1121
+ resolved_env (dict): Dictionary of resolved env vars (legacy
1122
+ mode) or placeholder strings (translate mode).
691
1123
  runtime_vars (dict): Dictionary of resolved runtime variables.
692
1124
 
693
1125
  Returns:
694
- str: Processed value with actual variable values.
1126
+ str: Processed value with placeholders translated or resolved.
695
1127
  """
696
1128
  import re
697
1129
 
@@ -700,23 +1132,33 @@ class CopilotClientAdapter(MCPClientAdapter):
700
1132
 
701
1133
  processed = str(value)
702
1134
 
703
- # Replace <TOKEN_NAME> with actual values from resolved_env (for Docker env vars)
704
- env_pattern = r"<([A-Z_][A-Z0-9_]*)>"
705
-
706
- def replace_env_var(match):
707
- env_name = match.group(1)
708
- return resolved_env.get(env_name, match.group(0)) # Return original if not found
709
-
710
- processed = re.sub(env_pattern, replace_env_var, processed)
711
-
712
- # Replace {runtime_var} with actual values from runtime_vars (for NPM args)
713
- runtime_pattern = r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}"
714
-
715
- def replace_runtime_var(match):
716
- var_name = match.group(1)
717
- return runtime_vars.get(var_name, match.group(0)) # Return original if not found
718
-
719
- processed = re.sub(runtime_pattern, replace_runtime_var, processed)
1135
+ if self._supports_runtime_env_substitution:
1136
+ # Track legacy <VAR> offenders before translating them away.
1137
+ self._last_legacy_angle_vars.update(_extract_legacy_angle_vars(processed))
1138
+ # Translate all three env-var placeholder syntaxes to ${VAR}.
1139
+ processed = _translate_env_placeholder(processed)
1140
+ else:
1141
+ # Replace <TOKEN_NAME> with actual values from resolved_env (for Docker env vars)
1142
+ env_pattern = r"<([A-Z_][A-Z0-9_]*)>"
1143
+
1144
+ def replace_env_var(match):
1145
+ env_name = match.group(1)
1146
+ return resolved_env.get(env_name, match.group(0)) # Return original if not found
1147
+
1148
+ processed = re.sub(env_pattern, replace_env_var, processed)
1149
+
1150
+ # Replace {runtime_var} with actual values from runtime_vars (for NPM args).
1151
+ # Negative lookbehind on `$` so we never re-substitute inside an already-translated
1152
+ # ${VAR} env placeholder (the brace is part of a Copilot CLI runtime substitution,
1153
+ # not an APM template variable).
1154
+ if runtime_vars:
1155
+ runtime_pattern = r"(?<!\$)\{([a-zA-Z_][a-zA-Z0-9_]*)\}"
1156
+
1157
+ def replace_runtime_var(match):
1158
+ var_name = match.group(1)
1159
+ return runtime_vars.get(var_name, match.group(0))
1160
+
1161
+ processed = re.sub(runtime_pattern, replace_runtime_var, processed)
720
1162
 
721
1163
  return processed
722
1164