cli-agent-runner 0.2.21__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.21 → cli_agent_runner-0.2.22}/.github/workflows/ci.yml +4 -4
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/workflows/release.yml +11 -7
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.vulture-whitelist.py +1 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/BACKLOG.md +0 -27
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/CHANGELOG.md +13 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/SECURITY.md +17 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/agent_runtime.py +35 -13
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/api.py +44 -6
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/api_types.py +5 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/defenses.py +6 -2
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/runner.py +6 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/startup_check.py +75 -7
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/commands.md +16 -2
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/container-pi.md +16 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/runbook.md +5 -3
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_container.py +118 -8
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_service.py +154 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_startup_check.py +51 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/.gitignore +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/LICENSE +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/README.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/README.zh.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/agent.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/memory.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/rounds.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_emit/serve.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_monitor_registry.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_monitor_state.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_plugin_scan.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_resolve.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_round_outcome.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_round_support.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_serve_policy.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_cgroup.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/_serve_round.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/clock.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/errors.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/loader.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/models.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/parsers.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/config/validators.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/host_health.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/phase_select.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/schedule.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/build.sh +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/deploy/Dockerfile +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/deploy/docker-compose.yml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/README.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/configuration.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/events.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/memory-efficiency.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/migrations/0.2.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/deploy-all-in-one.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/_clock.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.21 → 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.21 → 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.21 → 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.21 → cli_agent_runner-0.2.22}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_auto_stop_e2e.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_mem_floor.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_spawn_round_wedged.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_bdd_test_conventions.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_container_stop_bound.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_dataclass_fields_have_producers.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_event_int_coercion.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_import_footprint.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_migrations_registry_ssot.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_monitor_self_exclusion.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_internal_refs.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_price_logic.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_raw_time.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_vacuous_invariants.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_no_version_refs.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_phase_select_stateless.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_alloc_growth.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_log_decode_boundary.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_num_single_source.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_timeout_budget_invariant.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_marker_scan.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_reap.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cgroup_probe.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_claude_foreign_schema.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_main_boundary.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_code_precedence.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_coerce_int_poison.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_nested_strictness.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_strictness.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_control_char_config.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_detect_hung_latch.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_detector_isolation.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_effective_throttle_view.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_rollover.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_events_jsonl_decode.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_exec_prefix.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_exit_classifier.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_git_timeout.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_hook_redact_order.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_host_health.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_http_progress_epoch.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_interruptible_sleep_should_stop.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_cgroup_usage.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_metrics_swap.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_cmd_severity.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_identity.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_dedup_rearm.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_event_tail.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_loop.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_on_alert.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_round_deferred_stale.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_open_round_log.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_orphan_chain_single_pass.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_outer_round_ceiling.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_project_isolation.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_phase_select.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_plugin_scan_parity.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_primary_prompt_file_home.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_read_json_hardening.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_resolve.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_cmd_sigterm.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_outcome_agent_axis.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_runner_sidecar_ordering.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_mem_gate.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_phase_schedule.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_policy.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_cgroup_spine.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_oom_killed.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_round_terminate.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_service_mode.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_startup_battery_exit.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_supervisor_stale_self_blind.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_back_off_ladder.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_epoch_bound.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.21 → cli_agent_runner-0.2.22}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.2.21 → 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/
|
|
@@ -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,19 @@ 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
|
+
|
|
8
21
|
## [0.2.21] - 2026-09-11
|
|
9
22
|
|
|
10
23
|
### Added
|
|
@@ -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
|
|
@@ -57,6 +57,23 @@ a narrower safety net than the host-level reaping a bare agent process gets,
|
|
|
57
57
|
not a wider one — keep running it as a dedicated unprivileged user with no
|
|
58
58
|
passwordless sudo and tight egress limits regardless.
|
|
59
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
|
+
|
|
60
77
|
## Scope
|
|
61
78
|
|
|
62
79
|
In scope: the `agent_runner` Python package, its CLI, its bundled systemd
|
|
@@ -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
|
|
@@ -290,6 +291,14 @@ def resolve_exec_target(command0: str, work_dir: Path, env_path: str | None = No
|
|
|
290
291
|
# ladder) is out of scope here -- left for a future release.
|
|
291
292
|
_CONTAINER_RUNTIMES = frozenset({"docker", "podman"})
|
|
292
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
|
+
|
|
293
302
|
# docker/podman GLOBAL flags (before the subcommand) that take a separate
|
|
294
303
|
# value token -- skipped in pairs so `docker -H unix:///var/run/docker.sock
|
|
295
304
|
# run ...` / `docker --context foo run ...` still resolve to `run`. Not
|
|
@@ -507,14 +516,18 @@ def _best_effort_container_stop(
|
|
|
507
516
|
if cidfile is None:
|
|
508
517
|
return None, None
|
|
509
518
|
try:
|
|
510
|
-
|
|
519
|
+
with cidfile.open("rb") as fh:
|
|
520
|
+
raw = fh.read(_CIDFILE_READ_CAP + 1)
|
|
511
521
|
except OSError:
|
|
512
522
|
return None, None
|
|
513
|
-
if
|
|
514
|
-
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
|
|
515
528
|
try:
|
|
516
529
|
result = subprocess.run(
|
|
517
|
-
[runtime, "stop", "-t", str(REAP_GRACE_S), cid],
|
|
530
|
+
[runtime, "stop", "-t", str(REAP_GRACE_S), "--", cid],
|
|
518
531
|
stdout=subprocess.DEVNULL,
|
|
519
532
|
stderr=subprocess.PIPE,
|
|
520
533
|
timeout=_CONTAINER_STOP_TIMEOUT_S,
|
|
@@ -606,6 +619,11 @@ def run(
|
|
|
606
619
|
attempt where covered; it just isn't reported).
|
|
607
620
|
"""
|
|
608
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)
|
|
609
627
|
# Container-orphan defense: detect BEFORE building argv (spawn_command may
|
|
610
628
|
# gain an injected --cidfile) -- see _detect_container_run's module-level
|
|
611
629
|
# note. A non-container command's argv is byte-identical to before:
|
|
@@ -638,8 +656,13 @@ def run(
|
|
|
638
656
|
# no-silent-orphan floor.
|
|
639
657
|
pass
|
|
640
658
|
else:
|
|
641
|
-
|
|
642
|
-
|
|
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"
|
|
643
666
|
container_cidfile_is_ours = True
|
|
644
667
|
spawn_command = _inject_cidfile(command, run_idx, container_cidfile)
|
|
645
668
|
|
|
@@ -669,7 +692,6 @@ def run(
|
|
|
669
692
|
# PWD pinned last — it mirrors cwd= (a correctness pin, not a knob), so
|
|
670
693
|
# an [agent.env] PWD cannot silently diverge from where the child runs.
|
|
671
694
|
env = {**os.environ, **env_extra, "PWD": str(work_dir)}
|
|
672
|
-
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
673
695
|
proc: subprocess.Popen | None = None
|
|
674
696
|
log_file = None
|
|
675
697
|
try:
|
|
@@ -805,10 +827,10 @@ def run(
|
|
|
805
827
|
finally:
|
|
806
828
|
if log_file is not None:
|
|
807
829
|
log_file.close()
|
|
808
|
-
# Cleanup, not correctness: our own injected cidfile
|
|
809
|
-
# round-scoped
|
|
810
|
-
# (read-back for the best-effort stop, if any, already
|
|
811
|
-
# above). Only ours: an operator-supplied --cidfile is never
|
|
812
|
-
# 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.
|
|
813
835
|
if container_cidfile_is_ours and container_cidfile is not None:
|
|
814
|
-
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)
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
|
-
from agent_runner import events
|
|
17
|
+
from agent_runner import events, startup_check
|
|
18
18
|
from agent_runner.config import Config
|
|
19
19
|
|
|
20
20
|
|
|
@@ -29,6 +29,7 @@ class Defense:
|
|
|
29
29
|
|
|
30
30
|
def catalog(cfg: Config) -> list[Defense]:
|
|
31
31
|
"""Return the defense catalog parameterised by current config."""
|
|
32
|
+
check_kinds = startup_check.all_check_kinds()
|
|
32
33
|
return [
|
|
33
34
|
Defense(
|
|
34
35
|
name="round_timeout_s",
|
|
@@ -89,7 +90,10 @@ def catalog(cfg: Config) -> list[Defense]:
|
|
|
89
90
|
),
|
|
90
91
|
Defense(
|
|
91
92
|
name="startup_smoke_check",
|
|
92
|
-
|
|
93
|
+
# Computed from startup_check.all_check_kinds() -- the battery's
|
|
94
|
+
# own SSOT -- so this count/list can't drift from the real boot
|
|
95
|
+
# battery the way the hand-written "6 checks" string once did.
|
|
96
|
+
value=f"{len(check_kinds)} checks ({' / '.join(check_kinds)})",
|
|
93
97
|
codifies=(
|
|
94
98
|
"R721 + #446 — _common.md frontmatter caused 4h/123-round silent burn; "
|
|
95
99
|
"now halts serve (config_broken) instead of respawning a broken config"
|
|
@@ -78,6 +78,12 @@ def _write_holder_sidecar(lock_path: Path) -> None:
|
|
|
78
78
|
"started_at": now_iso_ms(),
|
|
79
79
|
"cmdline": _read_cmdline(os.getpid()),
|
|
80
80
|
}
|
|
81
|
+
import psutil
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
payload["create_time"] = psutil.Process(os.getpid()).create_time()
|
|
85
|
+
except psutil.Error:
|
|
86
|
+
pass
|
|
81
87
|
_holder_sidecar(lock_path).write_text(json.dumps(payload), encoding="utf-8")
|
|
82
88
|
|
|
83
89
|
|
|
@@ -106,6 +106,33 @@ def _check_stdin_container_interactive(
|
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def _check_control_plane_outside_container(
|
|
110
|
+
agent: AgentConfig, work_dir: Path, log_dir: Path, name: str
|
|
111
|
+
) -> CheckResult:
|
|
112
|
+
"""The supervisor's per-round pid and lock-holder files live under
|
|
113
|
+
log_dir. When the agent runs in a container and log_dir resolves inside
|
|
114
|
+
work_dir -- which the container mounts -- the agent could forge them;
|
|
115
|
+
refuse at boot. (The injected cidfile is kept safe separately, in a
|
|
116
|
+
host-private temp dir outside any mount.) Enforced on work_dir (the mount
|
|
117
|
+
we can reason about); exec_prefix's other mounts are opaque, so the
|
|
118
|
+
general rule is documented, not code-enforced."""
|
|
119
|
+
if agent_runtime._detect_container_run(agent.spawn_command(work_dir)) is None:
|
|
120
|
+
return CheckResult(name, True)
|
|
121
|
+
if log_dir.is_relative_to(work_dir):
|
|
122
|
+
return CheckResult(
|
|
123
|
+
name,
|
|
124
|
+
False,
|
|
125
|
+
reason="runtime.log_dir is inside work_dir, which the container mounts — "
|
|
126
|
+
"the agent could forge the pid/lock control files agent-runner trusts",
|
|
127
|
+
how_to_fix="set runtime.log_dir OUTSIDE work_dir (the default "
|
|
128
|
+
"~/.agent-runner/<project>/logs already is); agent-runner's "
|
|
129
|
+
"per-round pid and lock files live under log_dir, so keeping "
|
|
130
|
+
"log_dir outside the mount keeps them out of the agent's reach",
|
|
131
|
+
permanent=True,
|
|
132
|
+
)
|
|
133
|
+
return CheckResult(name, True)
|
|
134
|
+
|
|
135
|
+
|
|
109
136
|
def _check_work_dir_is_git(cfg: Config) -> CheckResult:
|
|
110
137
|
from agent_runner.vcs_state import GitTimeout, is_git_repo
|
|
111
138
|
|
|
@@ -205,6 +232,11 @@ CHECKS: list[Callable[[Config], CheckResult]] = [
|
|
|
205
232
|
]
|
|
206
233
|
|
|
207
234
|
|
|
235
|
+
def _phase_qualified(base: str, phase: str | None) -> str:
|
|
236
|
+
"""A check's name, suffixed `:<phase>` for an overriding phase profile."""
|
|
237
|
+
return base if phase is None else f"{base}:{phase}"
|
|
238
|
+
|
|
239
|
+
|
|
208
240
|
def _agent_cli_checks(cfg: Config) -> list[CheckResult]:
|
|
209
241
|
"""Validate ``command[0]`` for EVERY profile the runner might launch — the
|
|
210
242
|
base agent plus each phase's own agent — so a bad phase agent fails at boot
|
|
@@ -223,15 +255,24 @@ def _agent_cli_checks(cfg: Config) -> list[CheckResult]:
|
|
|
223
255
|
results: list[CheckResult] = []
|
|
224
256
|
for phase in [None, *overriding]:
|
|
225
257
|
profile = cfg.profile_for(phase)
|
|
226
|
-
|
|
227
|
-
results.append(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
work_dir = cfg.runtime.work_dir
|
|
259
|
+
results.append(
|
|
260
|
+
_check_agent_target(
|
|
261
|
+
profile.agent, work_dir, _phase_qualified("agent_cli_in_path", phase)
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
results.append(
|
|
265
|
+
_check_stdin_container_interactive(
|
|
266
|
+
profile.agent, work_dir, _phase_qualified("stdin_container_interactive", phase)
|
|
267
|
+
)
|
|
232
268
|
)
|
|
233
269
|
results.append(
|
|
234
|
-
|
|
270
|
+
_check_control_plane_outside_container(
|
|
271
|
+
profile.agent,
|
|
272
|
+
work_dir,
|
|
273
|
+
cfg.runtime.log_dir,
|
|
274
|
+
_phase_qualified("control_plane_outside_container", phase),
|
|
275
|
+
)
|
|
235
276
|
)
|
|
236
277
|
return results
|
|
237
278
|
|
|
@@ -254,6 +295,33 @@ def _phase_prompt_checks(cfg: Config) -> list[CheckResult]:
|
|
|
254
295
|
return results
|
|
255
296
|
|
|
256
297
|
|
|
298
|
+
def all_check_kinds() -> tuple[str, ...]:
|
|
299
|
+
"""Static kind-names ``run_battery`` can emit, ``:<phase>`` suffix
|
|
300
|
+
stripped: the 5 base ``CHECKS`` names plus the 3 per-profile kinds
|
|
301
|
+
``_agent_cli_checks`` adds (``agent_cli_in_path``,
|
|
302
|
+
``stdin_container_interactive``, ``control_plane_outside_container``).
|
|
303
|
+
Per-phase prompt checks (``_phase_prompt_checks``) reuse
|
|
304
|
+
``prompt_smoke_passes`` under a ``:<phase>`` suffix -- the same kind,
|
|
305
|
+
not a new one, so it is not listed twice.
|
|
306
|
+
|
|
307
|
+
Single source of truth for the defenses catalog's ``startup_smoke_check``
|
|
308
|
+
entry (see ``defenses.py``), so the human-readable count there is
|
|
309
|
+
computed from this instead of hand-copied and drifting; pinned against
|
|
310
|
+
the real battery by
|
|
311
|
+
``test_all_check_kinds_should_match_battery_kinds_when_config_is_valid``.
|
|
312
|
+
"""
|
|
313
|
+
return (
|
|
314
|
+
"config_loaded",
|
|
315
|
+
"log_dir_writable",
|
|
316
|
+
"work_dir_is_git_repo",
|
|
317
|
+
"prompt_file_exists",
|
|
318
|
+
"prompt_smoke_passes",
|
|
319
|
+
"agent_cli_in_path",
|
|
320
|
+
"stdin_container_interactive",
|
|
321
|
+
"control_plane_outside_container",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
257
325
|
def run_battery(cfg: Config) -> list[CheckResult]:
|
|
258
326
|
"""Run all checks. Returns empty list if escape hatch env is set."""
|
|
259
327
|
if os.environ.get(ESCAPE_HATCH_ENV, "").lower() in ("1", "true", "yes", "on"):
|
|
@@ -65,11 +65,20 @@ Generate and install systemd user unit(s):
|
|
|
65
65
|
- `--force`: overwrite a same-basename sibling project's unit (the clobber guard
|
|
66
66
|
otherwise refuses, e.g. after moving/renaming a project directory).
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
For the default (non-`--system`) path, after writing the unit(s) it runs
|
|
69
|
+
`systemctl --user daemon-reload`, `enable`, `start`. The `--system` path runs
|
|
70
|
+
the system-level `daemon-reload` and `enable` only, as noted above — it does
|
|
71
|
+
not start the unit.
|
|
69
72
|
|
|
70
73
|
### `agent-runner uninstall`
|
|
71
74
|
|
|
72
|
-
Stops and disables both units, then deletes the unit files and reloads
|
|
75
|
+
Stops and disables both user units, then deletes the unit files and reloads
|
|
76
|
+
systemd. A `--system`-installed unit is root-owned and out of reach here: this
|
|
77
|
+
refuses instead, printing the `sudo systemctl disable --now ...` command for
|
|
78
|
+
the serve unit (and the monitor unit too, if `--system --monitor` installed
|
|
79
|
+
one) to run in its place. Never raises for a missing user bus either (the
|
|
80
|
+
dietpi/RPi hosts `--system` is recommended for) — a failed `daemon-reload`
|
|
81
|
+
there is harmless once the units are already gone.
|
|
73
82
|
|
|
74
83
|
### `agent-runner start | stop | kill | restart | status`
|
|
75
84
|
|
|
@@ -79,6 +88,11 @@ there:
|
|
|
79
88
|
- `start` is idempotent.
|
|
80
89
|
- `stop` waits up to `round_timeout_s` for the current round.
|
|
81
90
|
- `kill` (and `restart --force`) is for a stuck round only — 5s grace then SIGKILL.
|
|
91
|
+
- `restart` against a `--system`-installed unit refuses, printing the
|
|
92
|
+
`sudo systemctl restart ...` command to run instead (it manages user-scope
|
|
93
|
+
units only). `status` still reports it correctly — a `--system` install
|
|
94
|
+
sets `system_managed: true` on the result rather than misreporting it as a
|
|
95
|
+
`systemd_user`-mode service.
|
|
82
96
|
|
|
83
97
|
### `agent-runner round`
|
|
84
98
|
|
|
@@ -87,6 +87,22 @@ dirty-tree detector every round:
|
|
|
87
87
|
-v /home/you/.pi-container-auth:/home/agent/.pi/agent:ro
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
## Residual: the config file is inside the mount
|
|
91
|
+
|
|
92
|
+
`-v {work_dir}:/work:Z` above mounts all of `work_dir` read-write, and
|
|
93
|
+
`agent-runner.toml` is scaffolded at the `work_dir` root — so it's inside
|
|
94
|
+
that mount too. agent-runner's own startup check keeps its pid and lock
|
|
95
|
+
files out of a mounted `work_dir` (they live under `runtime.log_dir`,
|
|
96
|
+
required to sit outside it — the injected cidfile is separately placed in a
|
|
97
|
+
host-private temp dir outside any mount, protected independently of
|
|
98
|
+
`log_dir`), but it does not and cannot protect the config file the same way:
|
|
99
|
+
a containerized agent that can write
|
|
100
|
+
its own mount can rewrite `[agent] command` / `exec_prefix` and have it run
|
|
101
|
+
on the host next round. This is a residual of the supervisor-not-sandbox
|
|
102
|
+
model (see [SECURITY.md](../../SECURITY.md#containment)), not something this
|
|
103
|
+
recipe or agent-runner defends against — if you don't fully trust the agent,
|
|
104
|
+
keep the config (and `runtime.stop_file`, if set) outside any bind-mount.
|
|
105
|
+
|
|
90
106
|
## Footguns
|
|
91
107
|
|
|
92
108
|
These aren't enforced by agent-runner (`exec_prefix` is opaque) — get them
|
|
@@ -729,11 +729,13 @@ ssh -L 8765:127.0.0.1:8765 <pi-host>
|
|
|
729
729
|
### Going truly 24×7 (systemd)
|
|
730
730
|
|
|
731
731
|
```bash
|
|
732
|
-
agent-runner install --config <toml>
|
|
733
|
-
systemctl --user start agent-runner@<project>
|
|
734
|
-
systemctl --user enable agent-runner@<project> # restart on Pi reboot
|
|
732
|
+
agent-runner install --monitor --config <toml>
|
|
735
733
|
```
|
|
736
734
|
|
|
735
|
+
`install` already enables, starts, and (with `--monitor`) installs the
|
|
736
|
+
auto-stop sidekick in one step — no separate `systemctl --user start/enable`
|
|
737
|
+
needed.
|
|
738
|
+
|
|
737
739
|
## Supervision coverage on a 24/7 host
|
|
738
740
|
|
|
739
741
|
Check this table before wrapping `[agent] command` in a shell script — most
|