cli-agent-runner 0.2.20__tar.gz → 0.2.22__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 (372) hide show
  1. cli_agent_runner-0.2.22/.github/dependabot.yml +6 -0
  2. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/workflows/ci.yml +4 -4
  3. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/workflows/release.yml +11 -7
  4. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.vulture-whitelist.py +2 -0
  5. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/BACKLOG.md +0 -27
  6. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/CHANGELOG.md +26 -0
  7. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/PKG-INFO +1 -1
  8. cli_agent_runner-0.2.22/SECURITY.md +84 -0
  9. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_version.py +2 -2
  10. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/agent_runtime.py +67 -20
  11. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/api.py +44 -6
  12. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/api_types.py +5 -0
  13. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/serve_cmd.py +13 -1
  14. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/models.py +11 -0
  15. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/parsers.py +9 -0
  16. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/defenses.py +6 -2
  17. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/runner.py +7 -1
  18. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/startup_check.py +111 -4
  19. cli_agent_runner-0.2.22/deploy/Dockerfile +24 -0
  20. cli_agent_runner-0.2.22/deploy/docker-compose.yml +18 -0
  21. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/README.md +2 -0
  22. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/commands.md +17 -3
  23. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/configuration.md +5 -1
  24. cli_agent_runner-0.2.22/docs/recipes/container-pi.md +195 -0
  25. cli_agent_runner-0.2.22/docs/recipes/deploy-all-in-one.md +49 -0
  26. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/runbook.md +5 -3
  27. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_architecture.py +1 -0
  28. cli_agent_runner-0.2.22/tests/invariants/test_container_stop_bound.py +18 -0
  29. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_container.py +163 -10
  30. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_service.py +154 -0
  31. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config.py +41 -0
  32. cli_agent_runner-0.2.22/tests/unit/test_exec_prefix.py +150 -0
  33. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_startup_check.py +102 -0
  34. cli_agent_runner-0.2.20/SECURITY.md +0 -47
  35. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.codecov.yml +0 -0
  36. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  37. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  38. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  39. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  40. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.gitignore +0 -0
  41. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/CODE_OF_CONDUCT.md +0 -0
  42. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/CONTRIBUTING.md +0 -0
  43. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/LICENSE +0 -0
  44. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/README.md +0 -0
  45. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/README.zh.md +0 -0
  46. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/__init__.py +0 -0
  47. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_docgen.py +0 -0
  48. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/__init__.py +0 -0
  49. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/agent.py +0 -0
  50. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/memory.py +0 -0
  51. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/rounds.py +0 -0
  52. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/serve.py +0 -0
  53. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_monitor_detectors.py +0 -0
  54. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_monitor_registry.py +0 -0
  55. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_monitor_state.py +0 -0
  56. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_plugin_scan.py +0 -0
  57. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_redact.py +0 -0
  58. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_registry.py +0 -0
  59. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_resolve.py +0 -0
  60. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_round_outcome.py +0 -0
  61. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_round_support.py +0 -0
  62. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_serve_policy.py +0 -0
  63. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_substrate.py +0 -0
  64. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_throttle.py +0 -0
  65. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/__init__.py +0 -0
  66. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/_constants.py +0 -0
  67. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
  68. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/codewhale.py +0 -0
  69. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
  70. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/gemini.py +0 -0
  71. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/kimi.py +0 -0
  72. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/pi.py +0 -0
  73. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/__init__.py +0 -0
  74. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/__main__.py +0 -0
  75. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_cgroup.py +0 -0
  76. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_round.py +0 -0
  77. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/common.py +0 -0
  78. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/events_cmd.py +0 -0
  79. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/init_cmd.py +0 -0
  80. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/install_cmd.py +0 -0
  81. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/migrate_cmd.py +0 -0
  82. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/monitor_cmd.py +0 -0
  83. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/peek_cmd.py +0 -0
  84. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/round_cmd.py +0 -0
  85. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/service_cmd.py +0 -0
  86. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/upgrade_cmd.py +0 -0
  87. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/clock.py +0 -0
  88. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/__init__.py +0 -0
  89. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/errors.py +0 -0
  90. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/loader.py +0 -0
  91. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/validators.py +0 -0
  92. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/context_store.py +0 -0
  93. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/events.py +0 -0
  94. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/hooks.py +0 -0
  95. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/host_health.py +0 -0
  96. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/http_progress.py +0 -0
  97. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/lifecycle.py +0 -0
  98. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/metrics.py +0 -0
  99. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/migrations.py +0 -0
  100. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/monitor.py +0 -0
  101. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/phase_select.py +0 -0
  102. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/__init__.py +0 -0
  103. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/aider.toml +0 -0
  104. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/claude.toml +0 -0
  105. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/codewhale.toml +0 -0
  106. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/gemini.toml +0 -0
  107. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/kimi.toml +0 -0
  108. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/pi.toml +0 -0
  109. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/prompt_loader.py +0 -0
  110. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/remote_relay.py +0 -0
  111. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/round_log.py +0 -0
  112. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/round_view.py +0 -0
  113. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/scaffold.py +0 -0
  114. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/schedule.py +0 -0
  115. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/service_unit.py +0 -0
  116. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/vcs_state.py +0 -0
  117. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/build.sh +0 -0
  118. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/architecture.md +0 -0
  119. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/events.md +0 -0
  120. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/long-running-agents.md +0 -0
  121. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/memory-efficiency.md +0 -0
  122. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/migrations/0.2.md +0 -0
  123. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/plugins.md +0 -0
  124. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/quickstart.md +0 -0
  125. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/aider.md +0 -0
  126. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/codewhale.md +0 -0
  127. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/kimi.md +0 -0
  128. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/pi.md +0 -0
  129. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/thesis.md +0 -0
  130. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/pyproject.toml +0 -0
  131. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/__init__.py +0 -0
  132. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/_clock.py +0 -0
  133. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/_test_helpers.py +0 -0
  134. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/conftest.py +0 -0
  135. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/contract/__init__.py +0 -0
  136. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/contract/test_public_api_surface.py +0 -0
  137. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/__init__.py +0 -0
  138. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/conftest.py +0 -0
  139. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_graceful_stop.py +0 -0
  140. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_install_systemd.py +0 -0
  141. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_monitor_remote.py +0 -0
  142. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
  143. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
  144. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
  145. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
  146. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/generate_vulture_whitelist.py +0 -0
  147. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/__init__.py +0 -0
  148. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_auto_stop_e2e.py +0 -0
  149. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_bounded_run.py +0 -0
  150. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_context_enricher_namespacing.py +0 -0
  151. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_dirty_handler_seam.py +0 -0
  152. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_fresh_eyes_signal.py +0 -0
  153. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_grace_kill_emission.py +0 -0
  154. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_install_dry_run.py +0 -0
  155. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_monitor_seeded.py +0 -0
  156. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_detector_loaded.py +0 -0
  157. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_owned_paths.py +0 -0
  158. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_real_flow.py +0 -0
  159. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_prompt_delivery_stdin.py +0 -0
  160. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
  161. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_scaffold_presets.py +0 -0
  162. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_serve_loop.py +0 -0
  163. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_mem_floor.py +0 -0
  164. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_wedged.py +0 -0
  165. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_substrate_fingerprint.py +0 -0
  166. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_transient_error_backoff.py +0 -0
  167. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/__init__.py +0 -0
  168. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/_docs.py +0 -0
  169. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/_event_scan.py +0 -0
  170. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_atomic_write_enforced.py +0 -0
  171. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_authored_facts_declared.py +0 -0
  172. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
  173. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_bdd_test_conventions.py +0 -0
  174. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_catalogs.py +0 -0
  175. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_classification_ssot.py +0 -0
  176. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_config_error_consistency.py +0 -0
  177. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_config_value_set_ssot.py +0 -0
  178. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
  179. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_builtin_plugins.py +0 -0
  180. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
  181. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_cli_claims.py +0 -0
  182. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_operator_surface.py +0 -0
  183. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
  184. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_transient_error_claims.py +0 -0
  185. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_usage_event_fields.py +0 -0
  186. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_generated.py +0 -0
  187. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_index_complete.py +0 -0
  188. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_entry_points_resolve.py +0 -0
  189. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_event_int_coercion.py +0 -0
  190. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kind_registry.py +0 -0
  191. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kinds_ssot.py +0 -0
  192. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_events_doc_contract.py +0 -0
  193. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
  194. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_hook_contract_docs.py +0 -0
  195. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_import_footprint.py +0 -0
  196. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_layer_2_loop_size.py +0 -0
  197. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_migrations_registry_ssot.py +0 -0
  198. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_module_boundaries.py +0 -0
  199. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_module_sizes.py +0 -0
  200. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_monitor_self_exclusion.py +0 -0
  201. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_ai_signatures.py +0 -0
  202. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_internal_refs.py +0 -0
  203. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_price_logic.py +0 -0
  204. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
  205. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_raw_time.py +0 -0
  206. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_vacuous_invariants.py +0 -0
  207. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_version_refs.py +0 -0
  208. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_peek_schema_version.py +0 -0
  209. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_phase_select_stateless.py +0 -0
  210. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_plugins_config_stable.py +0 -0
  211. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
  212. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
  213. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_alloc_growth.py +0 -0
  214. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_log_decode_boundary.py +0 -0
  215. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_num_single_source.py +0 -0
  216. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_result_stable.py +0 -0
  217. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
  218. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
  219. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_timeout_budget_invariant.py +0 -0
  220. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_upstream_schema_canary.py +0 -0
  221. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
  222. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/__init__.py +0 -0
  223. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/parser.py +0 -0
  224. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/test_parser.py +0 -0
  225. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/test_quickstart.py +0 -0
  226. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/__init__.py +0 -0
  227. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime.py +0 -0
  228. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_grace.py +0 -0
  229. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
  230. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_progress.py +0 -0
  231. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_reap.py +0 -0
  232. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_signal_name.py +0 -0
  233. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_assemble_prompt.py +0 -0
  234. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_events_stream.py +0 -0
  235. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_install.py +0 -0
  236. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_observation.py +0 -0
  237. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_read_round_num.py +0 -0
  238. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_resolve_phase.py +0 -0
  239. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_types.py +0 -0
  240. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_auto_stop_gating.py +0 -0
  241. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_catalogs.py +0 -0
  242. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cgroup_probe.py +0 -0
  243. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_claude_error_detector.py +0 -0
  244. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_claude_foreign_schema.py +0 -0
  245. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli.py +0 -0
  246. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_common.py +0 -0
  247. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_init_install.py +0 -0
  248. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_main_boundary.py +0 -0
  249. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_monitor_http.py +0 -0
  250. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_service_peek_monitor.py +0 -0
  251. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_upgrade.py +0 -0
  252. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_code_precedence.py +0 -0
  253. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_plugin.py +0 -0
  254. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_coerce_int_poison.py +0 -0
  255. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_fresh_eyes.py +0 -0
  256. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_max_rounds.py +0 -0
  257. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_nested_strictness.py +0 -0
  258. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_prompt_delivery.py +0 -0
  259. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_schedule.py +0 -0
  260. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_stop_file.py +0 -0
  261. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_strictness.py +0 -0
  262. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
  263. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_transient_error_action.py +0 -0
  264. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_context_store.py +0 -0
  265. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_control_char_config.py +0 -0
  266. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_default_dirty_handler.py +0 -0
  267. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_defenses.py +0 -0
  268. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_detect_hung_latch.py +0 -0
  269. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_detector_isolation.py +0 -0
  270. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_detector_protocol.py +0 -0
  271. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_dirty_handlers.py +0 -0
  272. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_docgen.py +0 -0
  273. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_effective_throttle_view.py +0 -0
  274. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_emit_config_migrated.py +0 -0
  275. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule.py +0 -0
  276. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
  277. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events.py +0 -0
  278. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd.py +0 -0
  279. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_rollover.py +0 -0
  280. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_tail.py +0 -0
  281. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_jsonl_decode.py +0 -0
  282. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_example_dirty_handler.py +0 -0
  283. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_example_plugin.py +0 -0
  284. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_exit_cause.py +0 -0
  285. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_exit_classifier.py +0 -0
  286. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_fresh_eyes_trigger.py +0 -0
  287. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_gemini_plugin.py +0 -0
  288. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_git_timeout.py +0 -0
  289. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_hook_failure_isolation.py +0 -0
  290. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_hook_redact_order.py +0 -0
  291. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_hooks.py +0 -0
  292. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_host_health.py +0 -0
  293. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress.py +0 -0
  294. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress_epoch.py +0 -0
  295. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_init_entry_points.py +0 -0
  296. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
  297. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_kimi_plugin.py +0 -0
  298. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_lifecycle.py +0 -0
  299. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_metrics.py +0 -0
  300. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_cgroup_usage.py +0 -0
  301. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_swap.py +0 -0
  302. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_migrate_cmd.py +0 -0
  303. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_migrations.py +0 -0
  304. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_assembly.py +0 -0
  305. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_cmd_severity.py +0 -0
  306. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_identity.py +0 -0
  307. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_rearm.py +0 -0
  308. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
  309. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
  310. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
  311. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detectors.py +0 -0
  312. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_event_tail.py +0 -0
  313. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_loop.py +0 -0
  314. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_on_alert.py +0 -0
  315. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_remote.py +0 -0
  316. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
  317. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_schedule_pause.py +0 -0
  318. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_network_blip_signal_guard.py +0 -0
  319. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_open_round_log.py +0 -0
  320. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_orphan_chain_single_pass.py +0 -0
  321. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_outer_round_ceiling.py +0 -0
  322. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_argparse.py +0 -0
  323. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_project_isolation.py +0 -0
  324. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_schedule.py +0 -0
  325. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_select.py +0 -0
  326. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_phase_agent_launch.py +0 -0
  327. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_phase_profiles.py +0 -0
  328. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_phase_select.py +0 -0
  329. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_pi_plugin.py +0 -0
  330. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_constants.py +0 -0
  331. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_scan_parity.py +0 -0
  332. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_presets.py +0 -0
  333. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_primary_prompt_file_home.py +0 -0
  334. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_prompt_loader.py +0 -0
  335. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_read_json_hardening.py +0 -0
  336. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_redact.py +0 -0
  337. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_resolve.py +0 -0
  338. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd.py +0 -0
  339. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd_sigterm.py +0 -0
  340. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_log_helpers.py +0 -0
  341. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome.py +0 -0
  342. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome_agent_axis.py +0 -0
  343. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_view.py +0 -0
  344. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_runner.py +0 -0
  345. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_runner_sidecar_ordering.py +0 -0
  346. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_runner_throttle.py +0 -0
  347. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_scaffold.py +0 -0
  348. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_schedule.py +0 -0
  349. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_cmd_bounded.py +0 -0
  350. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_config_broken.py +0 -0
  351. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_crash_loop.py +0 -0
  352. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_mem_gate.py +0 -0
  353. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_phase_schedule.py +0 -0
  354. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_policy.py +0 -0
  355. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round.py +0 -0
  356. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
  357. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_log.py +0 -0
  358. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_oom_killed.py +0 -0
  359. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_terminate.py +0 -0
  360. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_schedule_gate.py +0 -0
  361. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_sentinel.py +0 -0
  362. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_startup_hooks.py +0 -0
  363. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_service_mode.py +0 -0
  364. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_service_unit.py +0 -0
  365. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_startup_battery_exit.py +0 -0
  366. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_substrate.py +0 -0
  367. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
  368. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_back_off_ladder.py +0 -0
  369. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_epoch_bound.py +0 -0
  370. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_plugin_classification.py +0 -0
  371. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_upgrade_migrate.py +0 -0
  372. {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_vcs_state.py +0 -0
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -18,7 +18,7 @@ jobs:
18
18
  runs-on: ubuntu-latest
19
19
  timeout-minutes: 2
20
20
  steps:
21
- - uses: actions/checkout@v5
21
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
22
22
  with:
23
23
  # PR: scan only new commits the PR introduces (base..head).
24
24
  # Push to main: scan the just-pushed commits (before..after).
@@ -74,10 +74,10 @@ jobs:
74
74
  # granularity AND because coverage requires extra pytest flags not in
75
75
  # the local `test` task. CONTRIBUTING.md states the parity intentionally.
76
76
  steps:
77
- - uses: actions/checkout@v5
77
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
78
78
  with:
79
79
  fetch-depth: 0
80
- - uses: actions/setup-python@v6
80
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
81
81
  with:
82
82
  python-version: ${{ matrix.python }}
83
83
  cache: pip
@@ -108,7 +108,7 @@ jobs:
108
108
  run: pytest tests/literate/ -q
109
109
  - name: upload coverage
110
110
  if: matrix.python == '3.12' && matrix.os == 'ubuntu-latest'
111
- uses: codecov/codecov-action@v6
111
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
112
112
  with:
113
113
  files: ./coverage.xml
114
114
  fail_ci_if_error: false
@@ -26,10 +26,10 @@ jobs:
26
26
  runs-on: ubuntu-latest
27
27
  timeout-minutes: 10
28
28
  steps:
29
- - uses: actions/checkout@v5
29
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
30
30
  with:
31
31
  fetch-depth: 0
32
- - uses: actions/setup-python@v6
32
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
33
33
  with:
34
34
  python-version: "3.12"
35
35
  - name: install build
@@ -38,7 +38,7 @@ jobs:
38
38
  run: python -m build
39
39
  - name: list dist artifacts
40
40
  run: ls -la dist/
41
- - uses: actions/upload-artifact@v7
41
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
42
42
  with:
43
43
  name: dist
44
44
  path: dist/
@@ -53,11 +53,15 @@ jobs:
53
53
  permissions:
54
54
  id-token: write
55
55
  steps:
56
- - uses: actions/download-artifact@v8
56
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
57
57
  with:
58
58
  name: dist
59
59
  path: dist/
60
- - uses: pypa/gh-action-pypi-publish@release/v1
60
+ # SHA-pinned to release/v1's composite. NOTE: this pins the composite source;
61
+ # the image it pulls is still referenced by a moving ghcr tag, so a full pin
62
+ # would require the ghcr digest — deferred (needs read:packages + release-time
63
+ # validation). dependabot (github-actions) bumps this SHA.
64
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
61
65
  with:
62
66
  attestations: true
63
67
 
@@ -68,8 +72,8 @@ jobs:
68
72
  permissions:
69
73
  contents: write
70
74
  steps:
71
- - uses: actions/checkout@v5
72
- - uses: actions/download-artifact@v8
75
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
76
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
73
77
  with:
74
78
  name: dist
75
79
  path: dist/
@@ -110,6 +110,7 @@ pid
110
110
  uptime_s
111
111
  unit_file
112
112
  rate_limit
113
+ system_managed
113
114
 
114
115
  # agent_runner.api_types.SystemMetrics
115
116
  mem_total_mb
@@ -133,6 +134,7 @@ prompt_arg_template
133
134
  name
134
135
  env
135
136
  prompt_delivery
137
+ exec_prefix
136
138
 
137
139
  # agent_runner.config.models.Config
138
140
  agent
@@ -53,18 +53,6 @@ kind name is the version discriminator; a breaking change ships as
53
53
  `round_start_v2`). Reaching this failure requires a contract violation — but the
54
54
  suite would not report it.
55
55
 
56
- ## `remote_failure_tolerance_s` does not cover `subprocess.TimeoutExpired` — NEEDS_DESIGN
57
-
58
- The tolerance window covers ssh exit code 255 only. `monitor.run_remote_command`
59
- raises `MonitorRemoteError` on rc=255, and `api.monitor_loop`'s retry loop
60
- catches exactly that. A hung (not failed) ssh connection instead trips the
61
- `subprocess.run(timeout=...)` guard and raises `subprocess.TimeoutExpired`,
62
- which bypasses the window and kills the monitor.
63
-
64
- The rc=255-only scope is intentional and pinned by the monitor-signals design,
65
- and the documentation matches the code exactly — there is no drift here. Widening
66
- it changes a documented recovery path, so it is a design decision, not debt.
67
-
68
56
  ## `anomaly_repetitive_*` default — NEEDS_DESIGN
69
57
 
70
58
  `monitor.anomaly_repetitive_window` and `anomaly_repetitive_threshold` both
@@ -89,21 +77,6 @@ Deferred because closing it is a naming decision, not cleanup: no event kind
89
77
  carries the meaning "stashed but ref lost" (`orphan_stash_failed` would be wrong
90
78
  — the stash exists). Left as-is until that kind is designed.
91
79
 
92
- ## `_monitor_loop_iter`'s three fail-open guards are hand-duplicated
93
-
94
- `api._monitor_loop_iter` wraps the startup `monitor_started` emit, the
95
- per-tick poll, and `on_alert`'s per-alert dispatch each in its own
96
- `try/except Exception: warnings.warn(...)` block so a crash in any one of the
97
- three never ends monitor supervision. The three guards are structurally
98
- identical (catch, format a `type(e).__name__: {e}` warning, keep the loop
99
- alive) but hand-copied rather than sharing one implementation; `on_alert`'s
100
- own `emit()` closure already shows the pattern for factoring a shared guard.
101
-
102
- Collapsing them to one shared guard is behavior-preserving — it must not
103
- change which failures warn vs which propagate, nor the specific fallback
104
- each site takes on failure (sleep-and-retry after a poll failure,
105
- `verdict = "failed"` after an `on_alert` failure). Slated for 0.2.19.
106
-
107
80
  ## `oom_kill_delta` is named differently on its two events — NEEDS_DESIGN, DEFERRED to 0.3
108
81
 
109
82
  The same cgroup `memory.events.oom_kill` delta is emitted under two
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.22] - 2026-09-11
9
+
10
+ ### Fixed
11
+ - Container safety: agent-runner now refuses to boot when `runtime.log_dir` resolves inside a containerized `work_dir` — a contained agent could otherwise rewrite the pid/lock/cidfile control files the supervisor trusts. The best-effort container stop also validates the recovered container id (hex, length-bounded) and passes an end-of-options separator, and the injected `--cidfile` moves to a host-private directory, so a poisoned cidfile can no longer stop an unrelated container.
12
+ - `kill` / `restart --force` verify the lock-holder's start time before signaling, so a recycled pid is never sent a signal.
13
+ - System mode (`install --system`): `restart` / `uninstall` recognize a system unit and refuse with the exact `sudo systemctl …` command instead of a misleading one, and `uninstall` no longer aborts with an uncaught error on a host without a systemd user bus.
14
+
15
+ ### Security
16
+ - Release and CI GitHub Actions are pinned to commit SHAs, with Dependabot tracking updates.
17
+
18
+ ### Notes
19
+ - Not breaking: no `agent-runner migrate` step, no systemd unit change. `status --json` gains a `system_managed` field; existing fields unchanged. The one newly-rejected configuration is a containerized agent whose `runtime.log_dir` sits inside `work_dir` — move `log_dir` outside `work_dir` (the default already is).
20
+
21
+ ## [0.2.21] - 2026-09-11
22
+
23
+ ### Added
24
+ - `[agent] exec_prefix`: an optional token list prepended to the agent command at spawn (with `{work_dir}` substituted), so the agent can run inside a container you supply (`docker`/`podman run …`) while agent-runner supervises the resulting process from outside. The agent's identity — throttle, crash-loop, usage plugins — still keys on `command`, unchanged.
25
+ - Startup check: a stdin-delivery agent whose container `exec_prefix` omits `-i`/`--interactive` now fails loudly at boot instead of silently hanging until the round-timeout kill.
26
+ - Docs: container recipes (`docs/recipes/container-pi.md`, with Kimi/Claude/rootless-podman deltas), an all-in-one deploy recipe (`deploy/` + `docs/recipes/deploy-all-in-one.md`), and a `SECURITY.md` containment section.
27
+
28
+ ### Fixed
29
+ - The best-effort container stop no longer reports a false "stop failed" when a `--rm` container has already exited on its own; an already-removed container now counts as a successful stop.
30
+
31
+ ### Notes
32
+ - Not breaking: no `agent-runner migrate` step, no systemd unit change.
33
+
8
34
  ## [0.2.20] - 2026-09-09
9
35
 
10
36
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cli-agent-runner
3
- Version: 0.2.20
3
+ Version: 0.2.22
4
4
  Summary: Restart-on-exit supervisor for autonomous CLI agents
5
5
  Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
6
6
  Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
@@ -0,0 +1,84 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest tagged release. We do not back-port security fixes to older versions.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report security issues privately via GitHub Security Advisories:
10
+ <https://github.com/wan9yu/cli-agent-runner/security/advisories/new>
11
+
12
+ If GitHub Security Advisories is not workable for you, email
13
+ `wangyu@go2imagination.com` with the subject line `agent-runner security`.
14
+
15
+ Please **do not** open a public issue for security problems.
16
+
17
+ ## Response timeline
18
+
19
+ - Acknowledgement within 3 business days.
20
+ - An initial assessment within 7 business days.
21
+ - A fix and coordinated disclosure timeline once the scope is understood.
22
+
23
+ ## Threat model
24
+
25
+ agent-runner is a **supervisor**, not a sandbox. It provides observability
26
+ (structured events, peek/watch/monitor) and lifecycle safety (round timeouts,
27
+ process-group reaping, orphan stashing, auto-stop on critical alerts). It does
28
+ **not** confine the agent process: the agent runs with the invoking user's full
29
+ privileges and can read, write, and execute anything that user can.
30
+
31
+ Prompt injection from untrusted repository content is unsolved and out of the
32
+ supervisor's reach — a malicious file in the work tree can steer the agent, and
33
+ no defense in this project prevents that.
34
+
35
+ Operators supervising untrusted input should run the agent as a dedicated
36
+ unprivileged user, inside a container or VM, without passwordless sudo, and
37
+ with egress limits. The supervisor's defenses bound the blast radius of a
38
+ *misbehaving* agent, not a *hostile* one.
39
+
40
+ ## Containment
41
+
42
+ If you need to bound what the agent can reach, run it inside a container (or
43
+ VM) you supply, via `[agent] exec_prefix` — see
44
+ [docs/recipes/container-pi.md](docs/recipes/container-pi.md). The container
45
+ (or VM) enforces the isolation; agent-runner still just watches the
46
+ resulting process from outside, the same way it watches a bare agent
47
+ process.
48
+
49
+ One limit worth knowing: agent-runner's own process-tree defenses (reaping a
50
+ hung round's descendants) only see processes in its own subtree, and a
51
+ container-runtime-managed process usually isn't one of them. On a stuck
52
+ round, the one remaining lever is stopping the container by the id
53
+ agent-runner captured at spawn, which only works for a plain `docker
54
+ run …`/`podman run …` invocation — not one wrapped in `sudo` or other global
55
+ flags (see container-pi.md's footguns). Using a container this way gives you
56
+ a narrower safety net than the host-level reaping a bare agent process gets,
57
+ not a wider one — keep running it as a dedicated unprivileged user with no
58
+ passwordless sudo and tight egress limits regardless.
59
+
60
+ **Residual: `agent-runner.toml` lives inside the mount.** The startup check
61
+ above stops the agent's pid and lock files from sitting inside a mounted
62
+ `work_dir` (the injected cidfile is separately placed in a host-private temp
63
+ dir outside any mount, so it's protected independently of `log_dir`) — but
64
+ `agent-runner.toml` itself is scaffolded at the `work_dir` root, and the
65
+ container recipes in
66
+ [docs/recipes/container-pi.md](docs/recipes/container-pi.md) bind-mount all
67
+ of `work_dir` read-write. Each round re-reads the config, so a containerized
68
+ agent that can write its own mount can rewrite `[agent] command` /
69
+ `exec_prefix` — or `runtime.stop_file`, if it's pointed inside the mount —
70
+ and have it run on the host next round. This is consistent with the threat
71
+ model above: agent-runner bounds a *misbehaving* agent, not a *hostile* one,
72
+ and containment is the container/VM's job, not agent-runner's. An operator
73
+ who needs to contain a not-fully-trusted agent should keep the config (and
74
+ `runtime.stop_file`) outside any bind-mount, or otherwise treat the
75
+ container boundary as their own responsibility to shape.
76
+
77
+ ## Scope
78
+
79
+ In scope: the `agent_runner` Python package, its CLI, its bundled systemd
80
+ unit templates, and the CI / release workflows.
81
+
82
+ Out of scope: vulnerabilities in dependencies (psutil, etc.) — please
83
+ report those upstream. Misconfiguration of a downstream deployment is also
84
+ out of scope unless agent-runner's defaults caused it.
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.20'
22
- __version_tuple__ = version_tuple = (0, 2, 20)
21
+ __version__ = version = '0.2.22'
22
+ __version_tuple__ = version_tuple = (0, 2, 22)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -14,6 +14,7 @@ import re
14
14
  import shutil
15
15
  import signal
16
16
  import subprocess # noqa: TID251 — sanctioned subprocess caller
17
+ import tempfile
17
18
  import threading
18
19
  from collections.abc import Callable
19
20
  from dataclasses import dataclass
@@ -26,6 +27,11 @@ from agent_runner.clock import SYSTEM_CLOCK, Clock
26
27
 
27
28
  REAP_GRACE_S = 5
28
29
 
30
+ # Wall-clock bound on the best-effort `<runtime> stop` subprocess. Pinned by an
31
+ # invariant test to stay <= _serve_policy._ROUND_TERM_GRACE_S so serve's
32
+ # round-terminate escalation never SIGKILLs round_cmd mid-stop.
33
+ _CONTAINER_STOP_TIMEOUT_S = REAP_GRACE_S + 5
34
+
29
35
  # Backstop cap on the stored child "name" (see _live_children). The primary
30
36
  # source (kernel comm) is already bounded (15 bytes on Linux); this only
31
37
  # guards the argv[0]-basename fallback used when comm is unavailable.
@@ -285,6 +291,14 @@ def resolve_exec_target(command0: str, work_dir: Path, env_path: str | None = No
285
291
  # ladder) is out of scope here -- left for a future release.
286
292
  _CONTAINER_RUNTIMES = frozenset({"docker", "podman"})
287
293
 
294
+ # A real docker/podman container id is hex, 12-64 chars (short or full form).
295
+ # The cidfile is a trust-bearing control input -- validated before it's ever
296
+ # handed to a `stop` subprocess argv.
297
+ _CONTAINER_ID_RE = re.compile(r"^[0-9a-fA-F]{12,64}$")
298
+ # A real container id is <=64 hex chars; cap the read so a poisoned giant
299
+ # file can't OOM a constrained host.
300
+ _CIDFILE_READ_CAP = 128
301
+
288
302
  # docker/podman GLOBAL flags (before the subcommand) that take a separate
289
303
  # value token -- skipped in pairs so `docker -H unix:///var/run/docker.sock
290
304
  # run ...` / `docker --context foo run ...` still resolve to `run`. Not
@@ -362,6 +376,20 @@ def _detect_container_run(command: list[str]) -> tuple[str, int] | None:
362
376
  return None
363
377
 
364
378
 
379
+ # Matches the docker/podman flags that attach the container's stdin: `-i`, any
380
+ # short cluster containing `i` (`-it`, `-ti`, `-di`), and `--interactive[=…]`;
381
+ # NOT `--init` (double-dash, so the single-dash alternative can't consume it).
382
+ _INTERACTIVE_FLAG = re.compile(r"^(-[a-z]*i[a-z]*|--interactive(=.*)?)$")
383
+
384
+
385
+ def _exec_prefix_is_interactive(exec_prefix: list[str]) -> bool:
386
+ """True when the operator's ``exec_prefix`` carries a stdin-attaching flag.
387
+ Only the prefix's own docker flags matter -- the wrapped ``command`` runs
388
+ INSIDE the container, so an ``-i`` there is irrelevant to whether the
389
+ runtime forwards stdin."""
390
+ return any(_INTERACTIVE_FLAG.match(tok) for tok in exec_prefix)
391
+
392
+
365
393
  # `docker run`/`podman run` OPTIONS known to take a separate value token --
366
394
  # used only to walk PAST them without mistaking their value for IMAGE (e.g.
367
395
  # `--name foo`: without this, a naive scan would stop at "foo" thinking it's
@@ -479,26 +507,36 @@ def _best_effort_container_stop(
479
507
  ``(container_id, stop_ok)``: `container_id` is None when no id could be
480
508
  recovered (the container never actually started, or the cidfile is still
481
509
  empty) -- `stop_ok` is then also None, meaning no stop was attempted.
482
- Otherwise `stop_ok` is whether the `stop` subprocess exited zero. Every
483
- failure here is swallowed -- this is advisory best-effort, never a
484
- guarantee; the caller's loud warning + event is the actual floor when
485
- this doesn't land."""
510
+ Otherwise `stop_ok` is True when the `stop` subprocess exited zero, or
511
+ when it failed because the container was already gone (a `--rm`
512
+ container can self-remove before our stop lands -- that's not a failure
513
+ to report). Every other failure here is swallowed -- this is advisory
514
+ best-effort, never a guarantee; the caller's loud warning + event is the
515
+ actual floor when this doesn't land."""
486
516
  if cidfile is None:
487
517
  return None, None
488
518
  try:
489
- cid = cidfile.read_text(encoding="utf-8").strip()
519
+ with cidfile.open("rb") as fh:
520
+ raw = fh.read(_CIDFILE_READ_CAP + 1)
490
521
  except OSError:
491
522
  return None, None
492
- if not cid:
493
- return None, None
523
+ if len(raw) > _CIDFILE_READ_CAP:
524
+ return None, None # oversize — not a real container id
525
+ cid = raw.decode("utf-8", "replace").strip()
526
+ if not _CONTAINER_ID_RE.match(cid):
527
+ return None, None # not a hex id — detection already fired the loud warn + event
494
528
  try:
495
529
  result = subprocess.run(
496
- [runtime, "stop", cid],
530
+ [runtime, "stop", "-t", str(REAP_GRACE_S), "--", cid],
497
531
  stdout=subprocess.DEVNULL,
498
- stderr=subprocess.DEVNULL,
499
- timeout=10,
532
+ stderr=subprocess.PIPE,
533
+ timeout=_CONTAINER_STOP_TIMEOUT_S,
500
534
  )
501
- return cid, result.returncode == 0
535
+ if result.returncode == 0:
536
+ return cid, True
537
+ # A `--rm` container that already exited is GONE, not orphaned — not a failure.
538
+ stderr = (result.stderr or b"").decode("utf-8", "replace").lower()
539
+ return cid, "no such container" in stderr
502
540
  except (OSError, subprocess.TimeoutExpired):
503
541
  return cid, False
504
542
 
@@ -581,6 +619,11 @@ def run(
581
619
  attempt where covered; it just isn't reported).
582
620
  """
583
621
  stdin_mode = prompt_delivery == "stdin"
622
+ # Raising work (mkdir can fail on disk-full/perms/removed log_dir) must
623
+ # happen BEFORE the container-detection block below creates the
624
+ # host-private cidfile tempdir -- otherwise a raise here would skip the
625
+ # try/finally that cleans that tempdir up, leaking it once per round.
626
+ log_path.parent.mkdir(parents=True, exist_ok=True)
584
627
  # Container-orphan defense: detect BEFORE building argv (spawn_command may
585
628
  # gain an injected --cidfile) -- see _detect_container_run's module-level
586
629
  # note. A non-container command's argv is byte-identical to before:
@@ -613,8 +656,13 @@ def run(
613
656
  # no-silent-orphan floor.
614
657
  pass
615
658
  else:
616
- container_cidfile = log_path.with_name(log_path.name + ".cid")
617
- container_cidfile.unlink(missing_ok=True) # docker/podman refuse an existing one
659
+ # A supervisor control file must not live where the container can
660
+ # write it; put our injected cidfile in a host-private 0700 dir
661
+ # outside any mount. Docker writes the file host-side; the
662
+ # container never needs it.
663
+ container_cid_dir = Path(tempfile.mkdtemp(prefix="agent-runner-cid-"))
664
+ # Not created here — docker/podman refuse an existing cidfile.
665
+ container_cidfile = container_cid_dir / "round.cid"
618
666
  container_cidfile_is_ours = True
619
667
  spawn_command = _inject_cidfile(command, run_idx, container_cidfile)
620
668
 
@@ -644,7 +692,6 @@ def run(
644
692
  # PWD pinned last — it mirrors cwd= (a correctness pin, not a knob), so
645
693
  # an [agent.env] PWD cannot silently diverge from where the child runs.
646
694
  env = {**os.environ, **env_extra, "PWD": str(work_dir)}
647
- log_path.parent.mkdir(parents=True, exist_ok=True)
648
695
  proc: subprocess.Popen | None = None
649
696
  log_file = None
650
697
  try:
@@ -780,10 +827,10 @@ def run(
780
827
  finally:
781
828
  if log_file is not None:
782
829
  log_file.close()
783
- # Cleanup, not correctness: our own injected cidfile is a tiny,
784
- # round-scoped tmp file -- delete it once the round is fully done
785
- # (read-back for the best-effort stop, if any, already happened
786
- # above). Only ours: an operator-supplied --cidfile is never ours to
787
- # delete.
830
+ # Cleanup, not correctness: our own injected cidfile lives in a tiny,
831
+ # round-scoped private tempdir -- remove that whole dir once the round
832
+ # is fully done (read-back for the best-effort stop, if any, already
833
+ # happened above). Only ours: an operator-supplied --cidfile is never
834
+ # ours to delete.
788
835
  if container_cidfile_is_ours and container_cidfile is not None:
789
- container_cidfile.unlink(missing_ok=True)
836
+ shutil.rmtree(container_cidfile.parent, ignore_errors=True)
@@ -193,6 +193,11 @@ def init(
193
193
  _SYSTEM_UNITS_DIR = Path("/etc/systemd/system")
194
194
 
195
195
 
196
+ def _system_unit_exists(project: str) -> bool:
197
+ """A --system install's root-owned unit; detect_service_mode can't see it (user-scope only)."""
198
+ return (_SYSTEM_UNITS_DIR / serve_unit_filename(project)).exists()
199
+
200
+
196
201
  def _install_system(
197
202
  cfg: Config, project: str, *, config_path: Path, with_monitor: bool
198
203
  ) -> InstallResult:
@@ -283,6 +288,15 @@ def uninstall(work_dir: Path | None = None) -> bool:
283
288
  if work_dir is None:
284
289
  work_dir = Path.cwd()
285
290
  project = _project_name(work_dir)
291
+ if _system_unit_exists(project): # root-owned -- don't silently no-op the user-scope dir
292
+ units = [serve_unit_filename(project)]
293
+ if (_SYSTEM_UNITS_DIR / monitor_unit_filename(project)).exists():
294
+ units.append(monitor_unit_filename(project))
295
+ remedy = " && ".join(
296
+ f"sudo systemctl disable --now {u} && sudo rm {_SYSTEM_UNITS_DIR / u}" for u in units
297
+ )
298
+ print(f"{project} is managed by a system unit; run: {remedy}")
299
+ return False
286
300
  units_dir = lifecycle._user_systemd_dir()
287
301
  serve = units_dir / serve_unit_filename(project)
288
302
  monitor = units_dir / monitor_unit_filename(project)
@@ -295,7 +309,10 @@ def uninstall(work_dir: Path | None = None) -> bool:
295
309
  lifecycle.stop_unit_draining(p.name, clock=SYSTEM_CLOCK, confirm_s=_PID_SIGNAL_GRACE_S)
296
310
  _systemctl_user("disable", p.name)
297
311
  p.unlink(missing_ok=True)
298
- _systemctl_user("daemon-reload")
312
+ try:
313
+ _systemctl_user("daemon-reload")
314
+ except (subprocess.CalledProcessError, FileNotFoundError):
315
+ pass # no user bus on this host -- units above (if any) already gone
299
316
  return True
300
317
 
301
318
 
@@ -340,6 +357,8 @@ def _round_holder_pid(log_dir: Path) -> int | None:
340
357
  alive. This is the ONLY way ``kill()`` can reach an in-flight round: the
341
358
  round is ``start_new_session=True`` (its own session, its own pgid), so it
342
359
  sits outside whatever process group serve itself belongs to."""
360
+ import psutil
361
+
343
362
  from agent_runner.context_store import read_json
344
363
 
345
364
  data = read_json(log_dir / "agent-runner.lock.holder")
@@ -348,7 +367,17 @@ def _round_holder_pid(log_dir: Path) -> int | None:
348
367
  pid = data.get("pid")
349
368
  if not lifecycle._valid_pid(pid):
350
369
  return None
351
- return pid if pid_alive(pid) else None
370
+ if not pid_alive(pid):
371
+ return None
372
+ recorded = data.get("create_time")
373
+ if recorded is None:
374
+ return pid # legacy sidecar without a token: best-effort, mirrors serve.pid's read
375
+ try:
376
+ if abs(psutil.Process(pid).create_time() - recorded) < 1.0:
377
+ return pid
378
+ except psutil.Error:
379
+ return None
380
+ return None # recorded create_time != live process: a recycled pid, do not signal it
352
381
 
353
382
 
354
383
  def _terminate_round_pid(pid: int) -> None:
@@ -435,6 +464,11 @@ def restart(project: str | Path, *, force: bool = False) -> ServiceStatus:
435
464
  # SYSTEMD_USER unit, so restarting a PID_FILE/NONE service would stop it and
436
465
  # never bring it back — the half-execution this fix eliminates.
437
466
  pname = _resolve_project(project)
467
+ if _system_unit_exists(pname): # root-owned -- below's message would mislead
468
+ raise RuntimeError(
469
+ f"{pname} is managed by a system unit; run: "
470
+ f"sudo systemctl restart {serve_unit_filename(pname)}"
471
+ )
438
472
  log_dir = _log_dir_for_project(project)
439
473
  mode = detect_service_mode(pname, log_dir=log_dir)
440
474
  if mode != ServiceMode.SYSTEMD_USER:
@@ -463,14 +497,18 @@ def status(project: str | Path) -> ServiceStatus:
463
497
  pname = _resolve_project(project)
464
498
  log_dir = _log_dir_for_project(project)
465
499
  mode = detect_service_mode(pname, log_dir=log_dir)
466
- if mode == ServiceMode.PID_FILE:
467
- pid = PIDFile(log_dir / "serve.pid").read()
468
- return ServiceStatus(mode=mode, active=pid is not None and pid_alive(pid), pid=pid)
469
500
  if mode == ServiceMode.SYSTEMD_USER:
470
501
  unit = lifecycle._user_systemd_dir() / serve_unit_filename(pname)
471
502
  active = _systemd_active(serve_unit_filename(pname), log_dir)
472
503
  return ServiceStatus(mode=mode, active=active, unit_file=unit)
473
- return ServiceStatus(mode=ServiceMode.NONE, active=False)
504
+ # detect_service_mode only sees the user-scope unit dir; label a --system
505
+ # install found here (PID_FILE/NONE) via system_managed, not unit_file.
506
+ system_managed = _system_unit_exists(pname)
507
+ if mode == ServiceMode.PID_FILE:
508
+ pid = PIDFile(log_dir / "serve.pid").read()
509
+ active = pid is not None and pid_alive(pid)
510
+ return ServiceStatus(mode=mode, active=active, pid=pid, system_managed=system_managed)
511
+ return ServiceStatus(mode=ServiceMode.NONE, active=False, system_managed=system_managed)
474
512
 
475
513
 
476
514
  def _resolve_target(project: str | Path | None) -> tuple[str, Path]:
@@ -62,6 +62,11 @@ class ServiceStatus:
62
62
  uptime_s: float | None = None
63
63
  unit_file: Path | None = None
64
64
  rate_limit: RateLimitState | None = None
65
+ # True when a `--system` install's root-owned unit owns this project but
66
+ # detect_service_mode fell through to PID_FILE/NONE (it only sees the
67
+ # user-scope unit dir) -- distinct from `unit_file`, which stays the
68
+ # live, user-owned, CLI-restartable systemd_user unit's path.
69
+ system_managed: bool = False
65
70
 
66
71
 
67
72
  @dataclass(frozen=True)
@@ -29,6 +29,7 @@ from agent_runner._throttle import (
29
29
  round_outcome,
30
30
  round_was_mem_terminated,
31
31
  )
32
+ from agent_runner.agent_runtime import _detect_container_run
32
33
  from agent_runner.api import (
33
34
  check_self_terminated_sentinel,
34
35
  emit_config_broken,
@@ -611,7 +612,9 @@ def cmd(args) -> int:
611
612
  return fail_code
612
613
  stop_file = cfg.runtime.stop_file # cache: same pattern as effective_max_rounds
613
614
  work_dir = cfg.runtime.work_dir
614
- defer_to_cgroup = _probe_and_emit_cgroup_defer(log_dir)
615
+ # Emitted exactly once per serve lifetime -- the per-round defer decision below
616
+ # reuses this bool rather than re-probing/re-emitting every round.
617
+ cgroup_probe_defer = _probe_and_emit_cgroup_defer(log_dir)
615
618
  rounds_completed = 0
616
619
  # Three independent consecutive-failure counters, one per breaker: b12
617
620
  # crash-loop (unknown short crashes), mem-loop (mem-terminated
@@ -685,6 +688,15 @@ def cmd(args) -> int:
685
688
  ]
686
689
  if phase_arg is not None:
687
690
  round_argv += ["--phase", phase_arg]
691
+ # Phase-aware, per-round: a phase can override [phases.<name>.agent]
692
+ # command to a bare container run even when the base agent isn't one
693
+ # (exec_prefix is base-only, but command isn't) -- computing this once
694
+ # from the base agent would wrongly keep deferring a containerized
695
+ # phase's mid-round kill to dockerd's own cgroup-OOM.
696
+ defer_to_cgroup = cgroup_probe_defer and (
697
+ _detect_container_run(cfg.profile_for(phase_arg).agent.spawn_command(work_dir))
698
+ is None
699
+ )
688
700
  r_returncode = _spawn_round(
689
701
  round_argv,
690
702
  round_log_path,
@@ -30,6 +30,7 @@ class AgentConfig:
30
30
  name: str | None = None
31
31
  env: dict[str, str] = field(default_factory=dict)
32
32
  prompt_delivery: Literal["argv", "stdin"] = "argv"
33
+ exec_prefix: list[str] = field(default_factory=list)
33
34
 
34
35
  @property
35
36
  def binary(self) -> str | None:
@@ -40,6 +41,16 @@ class AgentConfig:
40
41
  serve and runner don't each re-spell ``Path(command[0]).name``."""
41
42
  return Path(self.command[0]).name if self.command else None
42
43
 
44
+ def spawn_command(self, work_dir: Path) -> list[str]:
45
+ """The effective argv to spawn: an opaque operator-supplied ``exec_prefix``
46
+ (e.g. ``docker run … img``) spliced BEFORE ``command``, with the one
47
+ ``{work_dir}`` token substituted IN THE PREFIX ONLY. ``command`` (and thus
48
+ ``binary``) is appended verbatim and never rewritten, so the
49
+ throttle/crash-loop/hook identity join stays keyed on the real agent. Empty
50
+ ``exec_prefix`` ⇒ ``command`` unchanged. The SSOT both the spawn (runner) and
51
+ the startup battery resolve, so they can't drift."""
52
+ return [t.replace("{work_dir}", str(work_dir)) for t in self.exec_prefix] + self.command
53
+
43
54
 
44
55
  @dataclass(frozen=True)
45
56
  class RuntimeConfig:
@@ -106,12 +106,16 @@ def _parse_agent(
106
106
  f"{field_prefix} command: must be a non-empty list; run `agent-runner migrate`"
107
107
  )
108
108
  env_d = _require_table(agent_d, "env", label=f"{field_prefix.strip('[]')}.env")
109
+ exec_prefix = _require_str_list(
110
+ agent_d.get("exec_prefix", []), field=f"{field_prefix} exec_prefix"
111
+ )
109
112
  return AgentConfig(
110
113
  command=command,
111
114
  prompt_arg_template=prompt_arg_template,
112
115
  name=agent_d.get("name"),
113
116
  env={str(k): str(v) for k, v in env_d.items()},
114
117
  prompt_delivery=prompt_delivery, # type: ignore[arg-type] # narrowed above
118
+ exec_prefix=exec_prefix,
115
119
  )
116
120
 
117
121
 
@@ -214,6 +218,11 @@ def _parse_phase_overrides(
214
218
  agent_sub = value["agent"]
215
219
  if not isinstance(agent_sub, dict):
216
220
  raise ConfigError(f"[phases.{phase_name}.agent] must be a table")
221
+ if "exec_prefix" in agent_sub:
222
+ raise ConfigError(
223
+ f"[phases.{phase_name}.agent] exec_prefix: not allowed per-phase — set "
224
+ "exec_prefix once on the base [agent] table (it applies to every phase)"
225
+ )
217
226
  merged = {**agent_d, **agent_sub}
218
227
  phase_agent = _parse_agent(
219
228
  merged,