cli-agent-runner 0.1.39__tar.gz → 0.1.40__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.1.39 → cli_agent_runner-0.1.40}/.gitignore +3 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/CHANGELOG.md +11 -1
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/_emit.py +13 -7
- cli_agent_runner-0.1.40/agent_runner/_redact.py +102 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/agent_runtime.py +27 -19
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/hooks.py +4 -3
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/commands.md +3 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/configuration.md +4 -3
- cli_agent_runner-0.1.40/docs/migrations/0.1.40.md +42 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/runbook.md +12 -1
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_grace_kill_emission.py +3 -3
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_agent_runtime_grace.py +47 -5
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_claude_error_detector.py +17 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_hooks.py +9 -0
- cli_agent_runner-0.1.40/tests/unit/test_redact.py +136 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/LICENSE +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/README.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/README.zh.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/api.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/api_types.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/events.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/build.sh +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/README.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/architecture.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/events.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/marketing/README.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/marketing/promo-cn.html +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/plugins.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/docs/thesis.md +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_vcs_state.py +0 -0
|
@@ -5,7 +5,17 @@ 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
|
-
## [
|
|
8
|
+
## [0.1.40] - 2026-05-31
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- Grace-kill child-process fields (`live_children` / `ignored_children` in `round_grace_extended` / `round_grace_kill`) no longer store raw command lines — only the executable basename + pid (and, for ignored children, which ignore-pattern matched). This structurally prevents secrets passed in a child's arguments from reaching `events-*.jsonl`. The field shape changed from a list of strings to a list of objects.
|
|
12
|
+
- Free-text event excerpts that can carry agent output — `transient_error_detected.raw`, `hook_failed.error_message`/`traceback`, `serve_startup_hook_failed.exc_msg` — are now best-effort redacted (auth headers, tokens, credential URLs, `KEY=value` secrets, known key-prefixes, JWT, PEM).
|
|
13
|
+
- Pre-0.1.40 `events-*.jsonl` may contain unredacted argv/excerpts — see `docs/migrations/0.1.40.md`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Docs: `configuration.md` `[monitor.host_health]` example points to the generated schema table instead of restating default values.
|
|
17
|
+
|
|
18
|
+
## [0.1.39] - 2026-05-29
|
|
9
19
|
|
|
10
20
|
### Fixed
|
|
11
21
|
- Grace-kill (`max_grace_after_result_s`) is no longer defeated by long-lived helper subprocesses (e.g. claude's persistent Bash-tool shell-snapshot). `[runtime] grace_kill_ignore_patterns` lists regexes for cmdlines to exclude from the liveness count; the claude preset ships a matching default.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.40
|
|
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
|
|
@@ -112,8 +112,10 @@ def emit_transient_error_detected(
|
|
|
112
112
|
raw: str,
|
|
113
113
|
) -> None:
|
|
114
114
|
"""Emit detection of a transient agent error (rate limit / 5xx / timeout)."""
|
|
115
|
+
from agent_runner._redact import redact_secrets
|
|
115
116
|
from agent_runner.events import TRANSIENT_ERROR_DETECTED, emit
|
|
116
117
|
|
|
118
|
+
raw = redact_secrets(raw)
|
|
117
119
|
emit(
|
|
118
120
|
log_dir,
|
|
119
121
|
TRANSIENT_ERROR_DETECTED,
|
|
@@ -234,12 +236,15 @@ def emit_round_grace_kill(
|
|
|
234
236
|
*,
|
|
235
237
|
round_num: int,
|
|
236
238
|
grace_s: int,
|
|
237
|
-
live_children: list[
|
|
239
|
+
live_children: list[dict] | None = None,
|
|
238
240
|
) -> None:
|
|
239
241
|
"""Emit when the subprocess was killed because the grace-after-result timer
|
|
240
242
|
expired AND the agent's process group had no live worker processes left
|
|
241
243
|
(a genuine hang). Distinct from round_grace_extended (grace elapsed but a
|
|
242
244
|
worker was still running) and round_timeout_kill (wall-clock exceeded).
|
|
245
|
+
|
|
246
|
+
live_children: list of ``{"name": <exe basename>, "pid": <int>}`` dicts
|
|
247
|
+
(0.1.40+; previously list of cmdline strings).
|
|
243
248
|
"""
|
|
244
249
|
from agent_runner.events import ROUND_GRACE_KILL, emit
|
|
245
250
|
|
|
@@ -257,17 +262,18 @@ def emit_round_grace_extended(
|
|
|
257
262
|
*,
|
|
258
263
|
round_num: int,
|
|
259
264
|
grace_s: int,
|
|
260
|
-
live_children: list[
|
|
261
|
-
ignored_children: list[
|
|
265
|
+
live_children: list[dict],
|
|
266
|
+
ignored_children: list[dict] | None = None,
|
|
262
267
|
) -> None:
|
|
263
268
|
"""Emit when the grace-after-result timer expired but the agent still had
|
|
264
269
|
live worker processes (e.g. a backgrounded build), so the round was NOT
|
|
265
270
|
killed; it continues until it finishes or hits round_timeout_s.
|
|
266
271
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
live_children: list of ``{"name": <exe basename>, "pid": <int>}`` dicts
|
|
273
|
+
(0.1.40+; previously list of cmdline strings).
|
|
274
|
+
ignored_children: list of ``{"name": ..., "pid": ..., "matched": <pattern>}``
|
|
275
|
+
dicts for children that matched a grace_kill_ignore_patterns entry
|
|
276
|
+
and were excluded from the liveness count (0.1.40+; previously cmdline strings).
|
|
271
277
|
"""
|
|
272
278
|
from agent_runner.events import ROUND_GRACE_EXTENDED, emit
|
|
273
279
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Mask secret-bearing tokens in semi-trusted free text before it is persisted
|
|
2
|
+
to durable, default-readable event logs (events-*.jsonl).
|
|
3
|
+
|
|
4
|
+
Single source of truth for secret-shaping. Pure, dependency-free, idempotent.
|
|
5
|
+
BEST-EFFORT: applied only to free-text excerpts (transient error output, hook
|
|
6
|
+
exception messages/tracebacks), never as the sole control. The grace-kill child
|
|
7
|
+
fields avoid this entirely by storing basename+pid, not argv.
|
|
8
|
+
|
|
9
|
+
Patterns are length/charset-anchored so benign argv (sk-report.md, psql -h db,
|
|
10
|
+
'Basic auth disabled') passes through unchanged.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
_MASK = "<redacted>"
|
|
18
|
+
|
|
19
|
+
_LONG_FLAGS = [
|
|
20
|
+
"--token",
|
|
21
|
+
"--password",
|
|
22
|
+
"--passwd",
|
|
23
|
+
"--api-key",
|
|
24
|
+
"--apikey",
|
|
25
|
+
"--secret",
|
|
26
|
+
"--secret-access-key",
|
|
27
|
+
"--access-key",
|
|
28
|
+
"--auth",
|
|
29
|
+
"--authorization",
|
|
30
|
+
"--client-secret",
|
|
31
|
+
"--aws-session-token",
|
|
32
|
+
"--bearer",
|
|
33
|
+
"--auth-token",
|
|
34
|
+
"--private-key",
|
|
35
|
+
]
|
|
36
|
+
# (prefix, min-tail-len) — anchored so short filenames don't match.
|
|
37
|
+
_PREFIX_RES = [
|
|
38
|
+
r"sk-ant-[A-Za-z0-9\-_]{16,}",
|
|
39
|
+
r"sk-[A-Za-z0-9]{16,}",
|
|
40
|
+
r"ghp_[A-Za-z0-9]{20,}",
|
|
41
|
+
r"gho_[A-Za-z0-9]{20,}",
|
|
42
|
+
r"ghs_[A-Za-z0-9]{20,}",
|
|
43
|
+
r"ghu_[A-Za-z0-9]{20,}",
|
|
44
|
+
r"github_pat_[A-Za-z0-9_]{20,}",
|
|
45
|
+
r"xox[bpars]-[A-Za-z0-9-]{10,}",
|
|
46
|
+
r"xapp-[A-Za-z0-9-]{10,}",
|
|
47
|
+
r"AKIA[0-9A-Z]{16}",
|
|
48
|
+
r"ASIA[0-9A-Z]{16}",
|
|
49
|
+
r"AIza[0-9A-Za-z_\-]{35}",
|
|
50
|
+
r"glpat-[A-Za-z0-9_\-]{20}",
|
|
51
|
+
r"ya29\.[A-Za-z0-9._\-]{20,}",
|
|
52
|
+
r"(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}",
|
|
53
|
+
r"npm_[A-Za-z0-9]{20,}",
|
|
54
|
+
r"hf_[A-Za-z0-9]{20,}",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
_FLAG_RE = re.compile(r"(?i)(" + "|".join(re.escape(f) for f in _LONG_FLAGS) + r")(\s+|=)(\S+)")
|
|
58
|
+
# Short HTTP-basic flag `-u user:pass` (curl/wget). Case-SENSITIVE so `-U`
|
|
59
|
+
# (psql/pg username) is left alone; a colon is required so `sort -u file` and
|
|
60
|
+
# bare `-u username` (no password) are not masked; and the value must NOT be a
|
|
61
|
+
# URL (`://`) — a `-u <url>` is left for _URL_USERINFO_RE, which masks only the
|
|
62
|
+
# userinfo and preserves the host (e.g. redis://<redacted>@cache:6379/0).
|
|
63
|
+
_SHORT_USER_RE = re.compile(r"(?<![\w-])-u(\s+|=)(?!\S*://)(\S+:\S+)")
|
|
64
|
+
_HEADER_NAME_RE = re.compile(
|
|
65
|
+
r"(?im)\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie|"
|
|
66
|
+
r"X-Api-Key|X-Auth-Token|X-Amz-Security-Token)(\s*:\s*)([^\r\n]+)"
|
|
67
|
+
)
|
|
68
|
+
_SCHEME_RE = re.compile(
|
|
69
|
+
r"\b(Bearer|Basic|Token|ApiKey|Digest|Negotiate|NTLM)(\s+)([A-Za-z0-9+/=._\-]{12,})"
|
|
70
|
+
)
|
|
71
|
+
_URL_USERINFO_RE = re.compile(r"(?i)([a-z][a-z0-9+.\-]*://)([^/\s@]+)@")
|
|
72
|
+
_URL_QUERY_RE = re.compile(
|
|
73
|
+
r"(?i)([?&#](?:access_token|api_?key|token|auth|secret|sig|signature|"
|
|
74
|
+
r"password|client_secret|x-amz-security-token|x-amz-signature)=)([^&#\s]+)"
|
|
75
|
+
)
|
|
76
|
+
_ENV_RE = re.compile(
|
|
77
|
+
r"(?i)((?:(?<=\s)|^)[A-Za-z_][A-Za-z0-9_]*"
|
|
78
|
+
r"(?:PASSWORD|PASSWD|PWD|TOKEN|SECRET|API[_-]?KEY|ACCESS[_-]?KEY|CREDENTIAL|PRIVATE[_-]?KEY)"
|
|
79
|
+
r"[A-Za-z0-9_]*)=(\S+)"
|
|
80
|
+
)
|
|
81
|
+
_PREFIX_RE = re.compile(r"(?<![A-Za-z0-9])(?:" + "|".join(_PREFIX_RES) + r")")
|
|
82
|
+
_JWT_RE = re.compile(r"\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+")
|
|
83
|
+
_PEM_RE = re.compile(
|
|
84
|
+
r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.DOTALL
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def redact_secrets(text: str) -> str:
|
|
89
|
+
"""Return *text* with secret-bearing tokens replaced by ``<redacted>``."""
|
|
90
|
+
if not text:
|
|
91
|
+
return text
|
|
92
|
+
out = _PEM_RE.sub(_MASK, text)
|
|
93
|
+
out = _ENV_RE.sub(rf"\1={_MASK}", out)
|
|
94
|
+
out = _FLAG_RE.sub(rf"\1\2{_MASK}", out)
|
|
95
|
+
out = _SHORT_USER_RE.sub(rf"-u\1{_MASK}", out)
|
|
96
|
+
out = _HEADER_NAME_RE.sub(rf"\1\2{_MASK}", out)
|
|
97
|
+
out = _SCHEME_RE.sub(rf"\1\2{_MASK}", out)
|
|
98
|
+
out = _URL_USERINFO_RE.sub(rf"\1{_MASK}@", out)
|
|
99
|
+
out = _URL_QUERY_RE.sub(rf"\1{_MASK}", out)
|
|
100
|
+
out = _JWT_RE.sub(_MASK, out)
|
|
101
|
+
out = _PREFIX_RE.sub(_MASK, out)
|
|
102
|
+
return out
|
|
@@ -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.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.40'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 40)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -31,7 +31,7 @@ class RunResult:
|
|
|
31
31
|
timed_out: bool
|
|
32
32
|
pid: int
|
|
33
33
|
killed_for_grace: bool = False
|
|
34
|
-
grace_kill_children: list[
|
|
34
|
+
grace_kill_children: list[dict] = field(default_factory=list)
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str) -> list[str]:
|
|
@@ -63,35 +63,43 @@ def _live_children(
|
|
|
63
63
|
*,
|
|
64
64
|
ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
65
65
|
max_n: int = 5,
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
) -> tuple[list[dict], list[dict]]:
|
|
67
|
+
"""Live (non-zombie) descendants of ``proc``, split into ``(live, ignored)``.
|
|
68
|
+
|
|
69
|
+
Each entry is ``{"name": <executable basename>, "pid": <int>}``; an ignored
|
|
70
|
+
entry also carries ``"matched": <pattern str>``. We store only basename+pid,
|
|
71
|
+
NOT argv — process arguments are where secrets leak (PGPASSWORD=…, --api-key
|
|
72
|
+
…, redis://:pass@…) and these lists are persisted to events-*.jsonl.
|
|
73
|
+
Ignore-pattern MATCHING runs against the full cmdline (detection unchanged);
|
|
74
|
+
only what we STORE is minimized.
|
|
74
75
|
"""
|
|
75
76
|
try:
|
|
76
77
|
parent = psutil.Process(proc.pid)
|
|
77
78
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
78
79
|
return [], []
|
|
79
|
-
live: list[
|
|
80
|
-
ignored: list[
|
|
80
|
+
live: list[dict] = []
|
|
81
|
+
ignored: list[dict] = []
|
|
81
82
|
for child in parent.children(recursive=True):
|
|
82
83
|
try:
|
|
83
84
|
if child.status() == psutil.STATUS_ZOMBIE:
|
|
84
85
|
continue
|
|
85
|
-
|
|
86
|
+
argv = child.cmdline()
|
|
87
|
+
full = " ".join(argv) or child.name() # MATCHING only
|
|
88
|
+
name = (Path(argv[0]).name if argv else "") or child.name()
|
|
89
|
+
pid = child.pid
|
|
86
90
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
87
91
|
continue
|
|
88
|
-
|
|
89
|
-
if ignore_patterns
|
|
92
|
+
matched = None
|
|
93
|
+
if ignore_patterns:
|
|
94
|
+
for p in ignore_patterns:
|
|
95
|
+
if p.search(full):
|
|
96
|
+
matched = p.pattern
|
|
97
|
+
break
|
|
98
|
+
if matched is not None:
|
|
90
99
|
if len(ignored) < max_n:
|
|
91
|
-
ignored.append(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
live.append(short)
|
|
100
|
+
ignored.append({"name": name, "pid": pid, "matched": matched})
|
|
101
|
+
elif len(live) < max_n:
|
|
102
|
+
live.append({"name": name, "pid": pid})
|
|
95
103
|
if len(live) >= max_n and len(ignored) >= max_n:
|
|
96
104
|
break
|
|
97
105
|
return live, ignored
|
|
@@ -114,7 +122,7 @@ def run(
|
|
|
114
122
|
max_grace_after_result_s: int = 0,
|
|
115
123
|
progress_callback: Callable[[dict], None] | None = None,
|
|
116
124
|
progress_interval_s: int = 0,
|
|
117
|
-
on_grace_extended: Callable[[list[
|
|
125
|
+
on_grace_extended: Callable[[list[dict], list[dict]], None] | None = None,
|
|
118
126
|
grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
119
127
|
) -> RunResult:
|
|
120
128
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
@@ -35,6 +35,7 @@ from pathlib import Path
|
|
|
35
35
|
from typing import Any, Protocol, runtime_checkable
|
|
36
36
|
|
|
37
37
|
from agent_runner import events
|
|
38
|
+
from agent_runner._redact import redact_secrets
|
|
38
39
|
from agent_runner._registry import ensure_unique
|
|
39
40
|
|
|
40
41
|
_HEAD_BYTES = 1024
|
|
@@ -201,7 +202,7 @@ def run_serve_startup_hooks(cfg: Any, log_dir: Path) -> bool:
|
|
|
201
202
|
hook(cfg)
|
|
202
203
|
except Exception as e: # noqa: BLE001 — hook is plugin contract; any failure aborts serve
|
|
203
204
|
exc_type = type(e).__name__
|
|
204
|
-
exc_msg = str(e)[:200]
|
|
205
|
+
exc_msg = redact_secrets(str(e))[:200]
|
|
205
206
|
print(
|
|
206
207
|
f"agent-runner: serve_startup_hook {hook.name} failed: {exc_type}: {exc_msg}",
|
|
207
208
|
file=sys.stderr,
|
|
@@ -232,6 +233,6 @@ def _summarize_error(exc: BaseException, tb: str) -> dict[str, str]:
|
|
|
232
233
|
trimmed = tb[:_HEAD_BYTES] + _TRUNC_MARKER + tb[-_TAIL_BYTES:]
|
|
233
234
|
return {
|
|
234
235
|
"error_type": type(exc).__name__,
|
|
235
|
-
"error_message": str(exc),
|
|
236
|
-
"traceback": trimmed,
|
|
236
|
+
"error_message": redact_secrets(str(exc)),
|
|
237
|
+
"traceback": redact_secrets(trimmed),
|
|
237
238
|
}
|
|
@@ -101,6 +101,9 @@ back to package-only mode automatically.
|
|
|
101
101
|
`--no-restart` forces package-only even on a systemd --user host (upgrade the
|
|
102
102
|
package now, restart your service yourself).
|
|
103
103
|
|
|
104
|
+
Operator walkthrough (per-deployment decision table, rollback, failure modes,
|
|
105
|
+
postmortem trail): see `docs/runbook.md` § "Upgrading agent-runner".
|
|
106
|
+
|
|
104
107
|
## Observation
|
|
105
108
|
|
|
106
109
|
### `agent-runner peek [flags]`
|
|
@@ -212,9 +212,10 @@ round_progress_interval_s = 0 # 0 = disabled; set >0 to emit round_progress hea
|
|
|
212
212
|
# supervisor_stale_threshold_s = 2700 # unset = round_timeout_s * 1.5; 0 = disable
|
|
213
213
|
|
|
214
214
|
[monitor.host_health]
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
# Thresholds for mem_pressure / disk_warning / disk_critical. Defaults are
|
|
216
|
+
# authoritative in the config-schema table above — set a field here only to
|
|
217
|
+
# override. (mem_avail_min_mb: mem_pressure when mem_available_mb below it;
|
|
218
|
+
# disk_warning_pct / disk_critical_pct: fire when disk_used_pct at/above.)
|
|
218
219
|
```
|
|
219
220
|
|
|
220
221
|
Comment out individual entries to disable; e.g. `# auto_stop_on = []` disables
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Migrating to 0.1.40
|
|
2
|
+
|
|
3
|
+
## TL;DR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install --upgrade cli-agent-runner==0.1.40
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
A security release. No config or behavior change for the supervisor. One
|
|
10
|
+
consumer-visible field-shape change (below).
|
|
11
|
+
|
|
12
|
+
## Breaking: grace-kill child fields are now objects, not strings
|
|
13
|
+
|
|
14
|
+
`round_grace_extended` / `round_grace_kill` previously carried
|
|
15
|
+
`live_children` / `ignored_children` as lists of command-line **strings**.
|
|
16
|
+
They are now lists of **objects**:
|
|
17
|
+
|
|
18
|
+
- `live_children`: `[{"name": "<exe basename>", "pid": <int>}, ...]`
|
|
19
|
+
- `ignored_children`: `[{"name": ..., "pid": ..., "matched": "<pattern>"}, ...]`
|
|
20
|
+
|
|
21
|
+
If you parse these fields, update your consumer. The change exists because the
|
|
22
|
+
old strings were full process command lines, which could contain a secret
|
|
23
|
+
passed in a child's arguments (a DB password, an API key, a credential URL).
|
|
24
|
+
Storing only basename + pid removes that leak by construction. Ignore-pattern
|
|
25
|
+
matching is unchanged — it still runs against the full command line; only what
|
|
26
|
+
is **stored** is minimized.
|
|
27
|
+
|
|
28
|
+
## Best-effort redaction of free-text excerpts
|
|
29
|
+
|
|
30
|
+
`transient_error_detected.raw`, `hook_failed.error_message`/`traceback`, and
|
|
31
|
+
`serve_startup_hook_failed.exc_msg` are now passed through a redactor that masks
|
|
32
|
+
auth headers, tokens, credential URLs, `KEY=value` secrets, known key-prefixes,
|
|
33
|
+
JWTs, and PEM blocks. This is **best-effort defense-in-depth**, not a guarantee:
|
|
34
|
+
do not pass secrets as command-line arguments to supervised agents, and treat
|
|
35
|
+
`events-*.jsonl` as sensitive.
|
|
36
|
+
|
|
37
|
+
## One-time action (if applicable)
|
|
38
|
+
|
|
39
|
+
If pre-0.1.40 `events-*.jsonl` were shipped to a log aggregator, shared, or
|
|
40
|
+
fetched cross-host (`monitor --host`), review them for secret-bearing child
|
|
41
|
+
argv / error excerpts and rotate any exposed credential. Post-upgrade events
|
|
42
|
+
are protected.
|
|
@@ -149,7 +149,18 @@ RestartSec=5
|
|
|
149
149
|
|
|
150
150
|
## Upgrading agent-runner
|
|
151
151
|
|
|
152
|
-
`upgrade` detects the deployment topology and takes the safe path for it
|
|
152
|
+
`upgrade` detects the deployment topology and takes the safe path for it. Pick
|
|
153
|
+
by how your service runs:
|
|
154
|
+
|
|
155
|
+
| Your deployment | How to upgrade | What happens |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| systemd `--user` (installed via `agent-runner install`) | `agent-runner upgrade [--target X.Y.Z]` | Full auto: graceful stop → pip → smoke → start, auto-rollback on smoke failure |
|
|
158
|
+
| systemd **system** unit / self-managed supervisor | `agent-runner upgrade --no-restart` then restart yourself | Package-only: pip + smoke (no service touched); you run `sudo systemctl restart <unit>` |
|
|
159
|
+
| container / pipx / fully hand-managed | `pip install --upgrade cli-agent-runner` then restart | Manual: you own both the install and the restart |
|
|
160
|
+
|
|
161
|
+
Whichever path: a long-running supervisor only loads the new code **after it
|
|
162
|
+
restarts** — that's why every non-`--user` path ends in a restart you run. The
|
|
163
|
+
three paths are detailed below.
|
|
153
164
|
|
|
154
165
|
### Path 1 — systemd --user service (installed via `agent-runner install`)
|
|
155
166
|
|
{cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/integration/test_grace_kill_emission.py
RENAMED
|
@@ -126,7 +126,7 @@ def test_round_grace_extended_emitted_when_worker_alive(tmp_path: Path) -> None:
|
|
|
126
126
|
assert len(extended_events) == 1
|
|
127
127
|
assert extended_events[0]["round_num"] == 1
|
|
128
128
|
assert extended_events[0]["grace_s"] == 1
|
|
129
|
-
assert any("
|
|
129
|
+
assert any(c["name"] == "sleep" for c in extended_events[0]["live_children"])
|
|
130
130
|
|
|
131
131
|
# round_grace_kill must NOT appear (round was busy, not idle)
|
|
132
132
|
grace_kill_events = [e for e in events if e.get("event") == "round_grace_kill"]
|
|
@@ -165,9 +165,9 @@ def test_round_grace_extended_carries_ignored_children(tmp_path: Path) -> None:
|
|
|
165
165
|
ev = extended_events[0]
|
|
166
166
|
|
|
167
167
|
# The plain sleep goes to live_children (real worker)
|
|
168
|
-
assert any("
|
|
168
|
+
assert any(c["name"] == "sleep" for c in ev["live_children"])
|
|
169
169
|
# The exec -a snapshot-bash-test process goes to ignored_children
|
|
170
|
-
assert any("snapshot-bash-test"
|
|
170
|
+
assert any(c["name"] == "snapshot-bash-test" for c in ev["ignored_children"])
|
|
171
171
|
|
|
172
172
|
# round_grace_kill must NOT appear (real worker still alive)
|
|
173
173
|
grace_kill_events = [e for e in events_list if e.get("event") == "round_grace_kill"]
|
|
@@ -97,7 +97,7 @@ def test_live_children_lists_backgrounded_child():
|
|
|
97
97
|
try:
|
|
98
98
|
time.sleep(0.5) # let the backgrounded child spawn
|
|
99
99
|
live, ignored = _live_children(p)
|
|
100
|
-
assert any("
|
|
100
|
+
assert any(c["name"] == "sleep" for c in live)
|
|
101
101
|
assert ignored == []
|
|
102
102
|
finally:
|
|
103
103
|
os.killpg(p.pid, signal.SIGKILL)
|
|
@@ -135,7 +135,7 @@ def test_grace_extended_when_result_but_child_running(tmp_path):
|
|
|
135
135
|
assert result.timed_out is True # round_timeout_s backstop reaped it
|
|
136
136
|
assert len(extended) == 1 # emitted once, not per-tick
|
|
137
137
|
live, ignored = extended[0]
|
|
138
|
-
assert any("
|
|
138
|
+
assert any(c["name"] == "sleep" for c in live)
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def test_grace_kill_after_child_exits_then_idle(tmp_path):
|
|
@@ -175,8 +175,8 @@ def test_live_children_splits_on_ignore_pattern():
|
|
|
175
175
|
time.sleep(0.5)
|
|
176
176
|
live, ignored = _live_children(p, ignore_patterns=[re.compile(r"snapshot-bash-")])
|
|
177
177
|
# One child should match the ignore pattern; the plain sleep goes to live.
|
|
178
|
-
assert any("snapshot-bash-"
|
|
179
|
-
assert any("
|
|
178
|
+
assert any(c["name"] in ("snapshot-bash-xyz", "sleep") for c in ignored)
|
|
179
|
+
assert any(c["name"] == "sleep" for c in live)
|
|
180
180
|
finally:
|
|
181
181
|
os.killpg(p.pid, signal.SIGKILL)
|
|
182
182
|
p.wait()
|
|
@@ -191,7 +191,7 @@ def test_live_children_no_patterns_preserves_0138_behavior():
|
|
|
191
191
|
time.sleep(0.5)
|
|
192
192
|
live, ignored = _live_children(p) # default None
|
|
193
193
|
assert ignored == []
|
|
194
|
-
assert any("
|
|
194
|
+
assert any(c["name"] == "sleep" for c in live)
|
|
195
195
|
finally:
|
|
196
196
|
os.killpg(p.pid, signal.SIGKILL)
|
|
197
197
|
p.wait()
|
|
@@ -224,3 +224,45 @@ def test_grace_kill_fires_when_only_ignored_helper_alive(tmp_path):
|
|
|
224
224
|
assert result.killed_for_grace is True
|
|
225
225
|
assert result.duration_s < 4
|
|
226
226
|
assert extended == [] # no extension emitted; reaped directly
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_live_children_stores_no_argv_secret():
|
|
230
|
+
"""A child with a secret in argv -> stored dict carries only name+pid; the
|
|
231
|
+
secret string never appears."""
|
|
232
|
+
from agent_runner.agent_runtime import _live_children
|
|
233
|
+
|
|
234
|
+
p = subprocess.Popen(
|
|
235
|
+
["bash", "-c", "exec -a 'tool --api-key sk-ant-SECRET123 x' sleep 30"],
|
|
236
|
+
start_new_session=True,
|
|
237
|
+
)
|
|
238
|
+
try:
|
|
239
|
+
time.sleep(0.5)
|
|
240
|
+
live, ignored = _live_children(p)
|
|
241
|
+
blob = repr(live + ignored)
|
|
242
|
+
assert "sk-ant-SECRET123" not in blob and "--api-key" not in blob
|
|
243
|
+
assert all(set(c) <= {"name", "pid", "matched"} for c in live + ignored)
|
|
244
|
+
finally:
|
|
245
|
+
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
|
246
|
+
p.wait()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_live_children_matched_records_pattern_not_argv():
|
|
250
|
+
"""Ignore-pattern matches on full cmdline; the stored ignored entry records
|
|
251
|
+
the matched pattern string + basename, never the full argv."""
|
|
252
|
+
from agent_runner.agent_runtime import _live_children
|
|
253
|
+
|
|
254
|
+
# Background a subshell that exec-replaces itself with the secret in argv[0].
|
|
255
|
+
# _live_children sees the child process; matching fires on its full cmdline.
|
|
256
|
+
p = subprocess.Popen(
|
|
257
|
+
["bash", "-c", "bash -c 'exec -a sk-MATCHME sleep 30' &\nwait"],
|
|
258
|
+
start_new_session=True,
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
time.sleep(0.5)
|
|
262
|
+
live, ignored = _live_children(p, ignore_patterns=[re.compile(r"sk-MATCHME")])
|
|
263
|
+
assert ignored and ignored[0]["matched"] == "sk-MATCHME"
|
|
264
|
+
# Only name/pid/matched stored — not the raw cmdline
|
|
265
|
+
assert all(set(c) == {"name", "pid", "matched"} for c in ignored)
|
|
266
|
+
finally:
|
|
267
|
+
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
|
268
|
+
p.wait()
|
{cli_agent_runner-0.1.39 → cli_agent_runner-0.1.40}/tests/unit/test_claude_error_detector.py
RENAMED
|
@@ -629,3 +629,20 @@ def test_given_claude_log_with_529_overloaded_when_classified_then_api_transient
|
|
|
629
629
|
)
|
|
630
630
|
parsed = _parse_claude_log(log)
|
|
631
631
|
assert parsed["transient_error"]["classification"] == "api_transient_5xx"
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def test_emit_transient_raw_redacted(tmp_path):
|
|
635
|
+
import json
|
|
636
|
+
|
|
637
|
+
from agent_runner._emit import emit_transient_error_detected
|
|
638
|
+
|
|
639
|
+
emit_transient_error_detected(
|
|
640
|
+
tmp_path,
|
|
641
|
+
classification="api_transient_5xx",
|
|
642
|
+
agent="claude",
|
|
643
|
+
reset_at_epoch=0,
|
|
644
|
+
round_num=1,
|
|
645
|
+
raw="boom Bearer sk-ant-LEAKvalue000111 tail",
|
|
646
|
+
)
|
|
647
|
+
payload = json.loads(sorted(tmp_path.glob("events-*.jsonl"))[-1].read_text().splitlines()[-1])
|
|
648
|
+
assert "sk-ant-LEAKvalue000111" not in payload["raw"] and "<redacted>" in payload["raw"]
|
|
@@ -177,3 +177,12 @@ def test_dry_run_propagated_to_hook_context(tmp_path) -> None:
|
|
|
177
177
|
|
|
178
178
|
assert captured, "post_round_hook was never called"
|
|
179
179
|
assert captured[0].dry_run is True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_summarize_error_redacts_message_and_traceback():
|
|
183
|
+
from agent_runner.hooks import _summarize_error
|
|
184
|
+
|
|
185
|
+
exc = RuntimeError("connect failed: postgresql://svc:S3cr3tPw0rd@db:5432/app")
|
|
186
|
+
out = _summarize_error(exc, tb="trace https://x-token:ghp_aaaaaaaaaaaaaaaaaaaa@h line 1")
|
|
187
|
+
assert "S3cr3tPw0rd" not in out["error_message"]
|
|
188
|
+
assert "ghp_aaaaaaaaaaaaaaaaaaaa" not in out["traceback"]
|