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.
- cli_agent_runner-0.2.22/.github/dependabot.yml +6 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/workflows/ci.yml +4 -4
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/workflows/release.yml +11 -7
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.vulture-whitelist.py +2 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/BACKLOG.md +0 -27
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/CHANGELOG.md +26 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/PKG-INFO +1 -1
- cli_agent_runner-0.2.22/SECURITY.md +84 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/agent_runtime.py +67 -20
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/api.py +44 -6
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/api_types.py +5 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/serve_cmd.py +13 -1
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/models.py +11 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/parsers.py +9 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/defenses.py +6 -2
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/runner.py +7 -1
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/startup_check.py +111 -4
- cli_agent_runner-0.2.22/deploy/Dockerfile +24 -0
- cli_agent_runner-0.2.22/deploy/docker-compose.yml +18 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/README.md +2 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/commands.md +17 -3
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/configuration.md +5 -1
- cli_agent_runner-0.2.22/docs/recipes/container-pi.md +195 -0
- cli_agent_runner-0.2.22/docs/recipes/deploy-all-in-one.md +49 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/runbook.md +5 -3
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_architecture.py +1 -0
- cli_agent_runner-0.2.22/tests/invariants/test_container_stop_bound.py +18 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_container.py +163 -10
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_service.py +154 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config.py +41 -0
- cli_agent_runner-0.2.22/tests/unit/test_exec_prefix.py +150 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_startup_check.py +102 -0
- cli_agent_runner-0.2.20/SECURITY.md +0 -47
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/.gitignore +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/LICENSE +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/README.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/README.zh.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/memory.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_serve_policy.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_cgroup.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_round.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/build.sh +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/events.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/memory-efficiency.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {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
- {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
- {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
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_mem_floor.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_import_footprint.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_timeout_budget_invariant.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_reap.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cgroup_probe.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_terminate.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.2.20 → cli_agent_runner-0.2.22}/tests/unit/test_vcs_state.py +0 -0
|
@@ -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
|
-
-
|
|
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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
this
|
|
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
|
-
|
|
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
|
|
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.
|
|
499
|
-
timeout=
|
|
532
|
+
stderr=subprocess.PIPE,
|
|
533
|
+
timeout=_CONTAINER_STOP_TIMEOUT_S,
|
|
500
534
|
)
|
|
501
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
|
784
|
-
# round-scoped
|
|
785
|
-
# (read-back for the best-effort stop, if any, already
|
|
786
|
-
# above). Only ours: an operator-supplied --cidfile is never
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|