cli-agent-runner 0.1.6__tar.gz → 0.1.8__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.8/.githooks/commit-msg +33 -0
- cli_agent_runner-0.1.8/.github/workflows/ci.yml +105 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/CHANGELOG.md +111 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/CONTRIBUTING.md +8 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/PKG-INFO +6 -5
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/README.md +5 -4
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/agent_runtime.py +8 -19
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/api.py +22 -2
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/api_types.py +2 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/common.py +6 -2
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/init_cmd.py +8 -2
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/config.py +8 -4
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/defenses.py +4 -4
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/events.py +6 -1
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/monitor.py +2 -2
- cli_agent_runner-0.1.8/agent_runner/presets/aider.toml +30 -0
- cli_agent_runner-0.1.8/agent_runner/presets/claude.toml +29 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/runner.py +4 -4
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/scaffold.py +26 -38
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/vcs_state.py +60 -1
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/README.md +9 -6
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/architecture.md +6 -6
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/configuration.md +33 -6
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/plugins.md +107 -10
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/quickstart.md +11 -6
- cli_agent_runner-0.1.8/docs/recipes/aider.md +117 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/runbook.md +14 -3
- cli_agent_runner-0.1.8/tests/_test_helpers.py +44 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/conftest.py +27 -0
- cli_agent_runner-0.1.8/tests/contract/test_public_api_surface.py +147 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_context_enricher_namespacing.py +2 -10
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_plugin_detector_loaded.py +2 -10
- cli_agent_runner-0.1.8/tests/integration/test_plugin_owned_paths.py +88 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_run_one_round_with_fake_agent.py +39 -0
- cli_agent_runner-0.1.8/tests/integration/test_scaffold_presets.py +80 -0
- cli_agent_runner-0.1.8/tests/invariants/test_module_sizes.py +22 -0
- cli_agent_runner-0.1.8/tests/invariants/test_no_ai_signatures.py +83 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_peek_schema_version.py +2 -2
- cli_agent_runner-0.1.8/tests/literate/__init__.py +0 -0
- cli_agent_runner-0.1.8/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_agent_runtime.py +23 -12
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_api_observation.py +59 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_api_types.py +33 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_cli_init_install.py +27 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_config.py +96 -14
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_defenses.py +26 -2
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_detector_protocol.py +3 -10
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_events.py +3 -10
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_hook_failure_isolation.py +2 -10
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_hooks.py +6 -17
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_monitor_detectors.py +5 -5
- cli_agent_runner-0.1.8/tests/unit/test_presets.py +87 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_scaffold.py +38 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_service_unit.py +1 -1
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_vcs_state.py +79 -0
- cli_agent_runner-0.1.6/.github/workflows/ci.yml +0 -57
- cli_agent_runner-0.1.6/tests/invariants/test_module_sizes.py +0 -46
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.gitignore +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/LICENSE +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/README.zh.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.6/tests → cli_agent_runner-0.1.8/agent_runner/presets}/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/build.sh +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/docs/commands.md +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.6/tests/e2e → cli_agent_runner-0.1.8/tests}/__init__.py +0 -0
- {cli_agent_runner-0.1.6/tests/integration → cli_agent_runner-0.1.8/tests/contract}/__init__.py +0 -0
- {cli_agent_runner-0.1.6/tests/invariants → cli_agent_runner-0.1.8/tests/e2e}/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.6/tests/literate → cli_agent_runner-0.1.8/tests/integration}/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_run_loop_backoff.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.6/tests/unit → cli_agent_runner-0.1.8/tests/invariants}/__init__.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.6 → cli_agent_runner-0.1.8}/tests/unit/test_startup_check.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Reject commit messages that contain AI-tool attribution.
|
|
3
|
+
#
|
|
4
|
+
# Rationale: this repository's history is curated to credit only the human
|
|
5
|
+
# author. AI-tool trailers (Co-Authored-By: Claude, 🤖 Generated with ..., etc.)
|
|
6
|
+
# create false "claude" / "anthropic" entries in GitHub's Contributors widget
|
|
7
|
+
# and bloat commit log noise.
|
|
8
|
+
#
|
|
9
|
+
# Activate per clone: git config core.hooksPath .githooks
|
|
10
|
+
# Bypass (rarely needed): git commit --no-verify
|
|
11
|
+
|
|
12
|
+
MSG_FILE="$1"
|
|
13
|
+
|
|
14
|
+
# Strip git-comment lines before checking
|
|
15
|
+
CONTENT=$(grep -v '^#' "$MSG_FILE" || true)
|
|
16
|
+
|
|
17
|
+
# Case-insensitive regex; tab/space tolerant
|
|
18
|
+
FORBIDDEN_REGEX='([Cc]o-[Aa]uthored-[Bb]y:|🤖|[Gg]enerated with [Cc]laude|[Gg]enerated with \[Cursor\]|noreply@anthropic\.com)'
|
|
19
|
+
|
|
20
|
+
if printf '%s\n' "$CONTENT" | grep -E -q "$FORBIDDEN_REGEX"; then
|
|
21
|
+
echo "" >&2
|
|
22
|
+
echo "commit-msg hook: AI-tool attribution detected in commit message." >&2
|
|
23
|
+
echo "" >&2
|
|
24
|
+
echo "Offending lines:" >&2
|
|
25
|
+
printf '%s\n' "$CONTENT" | grep -E -n "$FORBIDDEN_REGEX" | sed 's/^/ /' >&2
|
|
26
|
+
echo "" >&2
|
|
27
|
+
echo "Strip Co-Authored-By trailers, 🤖, 'Generated with ...', and" >&2
|
|
28
|
+
echo "noreply@anthropic.com refs, then retry." >&2
|
|
29
|
+
echo "" >&2
|
|
30
|
+
echo "(Bypass: git commit --no-verify — use sparingly, requires explicit" >&2
|
|
31
|
+
echo " review since CI lint-commits job will also reject the push.)" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
# Cancel in-flight runs on the same PR ref when a new push lands.
|
|
10
|
+
# Push-to-main runs always complete (cancel-in-progress only on PRs).
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ci-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint-commits:
|
|
17
|
+
name: lint commit messages (no AI-tool attribution)
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 2
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
with:
|
|
23
|
+
# PR: scan only new commits the PR introduces (base..head).
|
|
24
|
+
# Push to main: scan the just-pushed commits (before..after).
|
|
25
|
+
# fetch-depth=0 ensures both refs are available.
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
- name: scan new commit messages
|
|
28
|
+
env:
|
|
29
|
+
GH_EVENT: ${{ github.event_name }}
|
|
30
|
+
BEFORE_SHA: ${{ github.event.before }}
|
|
31
|
+
BASE_REF: ${{ github.event.pull_request.base.sha }}
|
|
32
|
+
HEAD_REF: ${{ github.event.pull_request.head.sha }}
|
|
33
|
+
run: |
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
if [ "$GH_EVENT" = "pull_request" ]; then
|
|
36
|
+
RANGE="${BASE_REF}..${HEAD_REF}"
|
|
37
|
+
else
|
|
38
|
+
# push event; before is 0..0 on first push or branch creation
|
|
39
|
+
if [ -z "${BEFORE_SHA:-}" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
|
40
|
+
RANGE="HEAD~1..HEAD"
|
|
41
|
+
else
|
|
42
|
+
RANGE="${BEFORE_SHA}..HEAD"
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
echo "Scanning commit-message bodies in $RANGE"
|
|
46
|
+
# Collect messages joined by NUL bytes; grep -P for fixed alternation.
|
|
47
|
+
BODIES=$(git log --format='%B%x00' "$RANGE" || true)
|
|
48
|
+
if [ -z "$BODIES" ]; then
|
|
49
|
+
echo " (no commits in range)"
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
PATTERN='(Co-Authored-By:|🤖|Generated with Claude|Generated with \[Cursor\]|noreply@anthropic\.com)'
|
|
53
|
+
if printf '%s' "$BODIES" | grep -i -E -q "$PATTERN"; then
|
|
54
|
+
echo "::error::AI-tool attribution detected in new commit messages."
|
|
55
|
+
echo "Offending lines:"
|
|
56
|
+
printf '%s' "$BODIES" | grep -i -n -E "$PATTERN" || true
|
|
57
|
+
echo ""
|
|
58
|
+
echo "Strip Co-Authored-By trailers, 🤖, 'Generated with ...',"
|
|
59
|
+
echo "and noreply@anthropic.com refs, then force-push the fixed range."
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
echo " clean"
|
|
63
|
+
|
|
64
|
+
test:
|
|
65
|
+
name: test py${{ matrix.python }} / ${{ matrix.os }}
|
|
66
|
+
runs-on: ${{ matrix.os }}
|
|
67
|
+
timeout-minutes: 15
|
|
68
|
+
strategy:
|
|
69
|
+
fail-fast: false
|
|
70
|
+
matrix:
|
|
71
|
+
python: ["3.11", "3.12", "3.13"]
|
|
72
|
+
os: [ubuntu-latest, macos-latest]
|
|
73
|
+
# Steps below mirror ./build.sh check, inlined for per-step Actions UI
|
|
74
|
+
# granularity AND because coverage requires extra pytest flags not in
|
|
75
|
+
# the local `test` task. CONTRIBUTING.md states the parity intentionally.
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
with:
|
|
79
|
+
fetch-depth: 0
|
|
80
|
+
- uses: actions/setup-python@v5
|
|
81
|
+
with:
|
|
82
|
+
python-version: ${{ matrix.python }}
|
|
83
|
+
cache: pip
|
|
84
|
+
- name: install
|
|
85
|
+
run: pip install -e ".[dev]"
|
|
86
|
+
- name: ruff
|
|
87
|
+
run: |
|
|
88
|
+
ruff check .
|
|
89
|
+
ruff format --check .
|
|
90
|
+
- name: tests with coverage
|
|
91
|
+
run: pytest -q --ignore=tests/e2e --ignore=tests/literate --cov --cov-report=xml --cov-report=term
|
|
92
|
+
- name: docs CI gate
|
|
93
|
+
run: |
|
|
94
|
+
python -m agent_runner._docgen
|
|
95
|
+
git diff --exit-code docs/
|
|
96
|
+
- name: literate quickstart
|
|
97
|
+
run: pytest tests/literate/ -q
|
|
98
|
+
- name: upload coverage
|
|
99
|
+
if: matrix.python == '3.12' && matrix.os == 'ubuntu-latest'
|
|
100
|
+
uses: codecov/codecov-action@v4
|
|
101
|
+
with:
|
|
102
|
+
files: ./coverage.xml
|
|
103
|
+
fail_ci_if_error: false
|
|
104
|
+
env:
|
|
105
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -7,6 +7,117 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.8] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Acknowledgements
|
|
13
|
+
|
|
14
|
+
Thanks to the argus-gateway team for Phase 4 dogfooding feedback that drove
|
|
15
|
+
every item in this release. 3 audit memos (~90KB) silently swept into an
|
|
16
|
+
orphan stash is a real-world failure mode; this release closes that loop.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `agent_runner.vcs_state.register_plugin_owned_paths()` — plugins opt-out
|
|
21
|
+
files/dirs from orphan-stash defense. Matching: trailing-slash prefix or
|
|
22
|
+
`pathlib.PurePath.match` glob (recognizes `**` for recursive segments via
|
|
23
|
+
`fnmatch` fallback on Python 3.11). Call at module import (entry_point
|
|
24
|
+
side-effect).
|
|
25
|
+
- `agent_runner.vcs_state.plugin_owned_paths()` — snapshot accessor for peek.
|
|
26
|
+
- `ProjectState.recent_hook_failures: list[dict]` — last 10 `hook_failed`
|
|
27
|
+
events filtered from `recent_events` for debugging hook integration.
|
|
28
|
+
- peek schema bumped 1.4 → 1.5. `plugins` block now includes
|
|
29
|
+
`pre_round_hooks`, `post_round_hooks`, `owned_paths` lists.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `docs/plugins.md` register-pattern examples corrected: registration must
|
|
34
|
+
happen as module-top side effect; entry_point loaders only import, they
|
|
35
|
+
do not invoke. Old `_register()` wrapper pattern silently didn't fire.
|
|
36
|
+
- `docs/plugins.md` gained "Declaring plugin-owned paths" and "Plugin tests
|
|
37
|
+
+ consumer pytest collision" sections.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Plugin outputs in plugin-declared paths (e.g. `proposals/`,
|
|
42
|
+
`logs/plugins/my_plugin/`) no longer silently swept into orphan stashes
|
|
43
|
+
by `process_orphan_wip`. Previously: 90KB Argus audit memos invisible
|
|
44
|
+
after Phase 4 round; required stash archaeology to recover.
|
|
45
|
+
|
|
46
|
+
### Migration
|
|
47
|
+
|
|
48
|
+
No breaking changes. Plugin authors:
|
|
49
|
+
|
|
50
|
+
- If your plugin writes files to `work_dir` and they keep getting stashed
|
|
51
|
+
between rounds, opt them out:
|
|
52
|
+
```python
|
|
53
|
+
from agent_runner.vcs_state import register_plugin_owned_paths
|
|
54
|
+
register_plugin_owned_paths(["your-output-dir/", "logs/your-plugin/**/*"])
|
|
55
|
+
```
|
|
56
|
+
- If you followed the old `_register()` pattern from docs and noticed
|
|
57
|
+
registrations not firing: move the call to module top:
|
|
58
|
+
```python
|
|
59
|
+
# was: def _register(): register_pre_round_hook(MyHook())
|
|
60
|
+
# now: register_pre_round_hook(MyHook()) # module-top side-effect
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## [0.1.7] - 2026-05-13
|
|
64
|
+
|
|
65
|
+
### Migration for existing 0.1.6 users (DOWNSTREAM CONSUMERS READ THIS)
|
|
66
|
+
|
|
67
|
+
If you maintain an `agent-runner.toml` by hand (rather than via `agent-runner init`),
|
|
68
|
+
you must add an `[agent.env]` block to preserve the Claude self-update suppression
|
|
69
|
+
that 0.1.6 injected implicitly. **Without this, mid-loop self-updates can race with
|
|
70
|
+
the supervisor.**
|
|
71
|
+
|
|
72
|
+
Add to your `agent-runner.toml`:
|
|
73
|
+
|
|
74
|
+
```toml
|
|
75
|
+
[agent.env]
|
|
76
|
+
DISABLE_AUTOUPDATER = "1"
|
|
77
|
+
CLAUDE_CODE_EFFORT_LEVEL = "xhigh"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or regenerate cleanly:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
agent-runner init --preset claude --force
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Plugin authors (Argus Gateway, etc.): no public API was renamed or removed
|
|
87
|
+
from your import surface. The deleted symbols (`agent_runner.agent_runtime.CRITICAL_ENV_DEFAULTS`,
|
|
88
|
+
`agent_runner.agent_runtime.merge_critical_envs`) were internal — not part of
|
|
89
|
+
the documented plugin API. A new public-API contract test
|
|
90
|
+
(`tests/contract/test_public_api_surface.py`) locks in `api_types`, `events`,
|
|
91
|
+
`hooks`, `monitor`, `detector_helpers` import surfaces so future refactors
|
|
92
|
+
can't silently drop names you rely on.
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- `agent-runner init --preset {claude,aider}` selects between bundled CLI presets
|
|
96
|
+
(default: `claude`). New preset directory: `agent_runner/presets/`.
|
|
97
|
+
- `[agent.env]` TOML block — per-CLI env injections, replacing the hardcoded
|
|
98
|
+
`CRITICAL_ENV_DEFAULTS` constant. Empty dict by default.
|
|
99
|
+
- `docs/recipes/aider.md` — aider integration recipe.
|
|
100
|
+
- `tests/contract/test_public_api_surface.py` — public-API surface snapshot for
|
|
101
|
+
plugin authors.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
- Core code (`agent_runtime.py`, `config.py`, `runner.py`, `scaffold.py`,
|
|
105
|
+
`defenses.py`) is now truly provider-agnostic: zero hardcoded Claude defaults.
|
|
106
|
+
Claude remains the reference example throughout the docs, but its specifics
|
|
107
|
+
live in `agent_runner/presets/claude.toml` (shipped as package data).
|
|
108
|
+
- `MonitorConfig.auth_fail_hint` default is now empty string; per-CLI hint
|
|
109
|
+
comes from the preset.
|
|
110
|
+
- `PEEK_SCHEMA_VERSION` bumped 1.3 → 1.4. `InitResult` gains `preset: str` field.
|
|
111
|
+
- `agent_runner/defenses.py` `critical_envs_injection` row now reads from
|
|
112
|
+
`cfg.agent.env.keys()` (not the deleted constant); state is "active" iff
|
|
113
|
+
the config defines any env injections, else "off".
|
|
114
|
+
|
|
115
|
+
### Removed
|
|
116
|
+
- `agent_runner.agent_runtime.CRITICAL_ENV_DEFAULTS` constant.
|
|
117
|
+
- `agent_runner.agent_runtime.merge_critical_envs()` function.
|
|
118
|
+
- `agent_runner.config._DEFAULT_AUTH_HINT` constant's Claude-specific default
|
|
119
|
+
string (replaced with `""`; per-CLI hint now in preset files).
|
|
120
|
+
|
|
10
121
|
## [0.1.6] - 2026-05-12
|
|
11
122
|
|
|
12
123
|
Zero-feature maintenance release — internal cleanup pass after the 0.1.x plugin
|
|
@@ -9,6 +9,7 @@ git clone https://github.com/wan9yu/cli-agent-runner.git
|
|
|
9
9
|
cd cli-agent-runner
|
|
10
10
|
python3 -m venv .venv && source .venv/bin/activate
|
|
11
11
|
pip install -e ".[dev]"
|
|
12
|
+
git config core.hooksPath .githooks # enables the commit-msg lint hook
|
|
12
13
|
./build.sh check
|
|
13
14
|
```
|
|
14
15
|
|
|
@@ -16,6 +17,13 @@ pip install -e ".[dev]"
|
|
|
16
17
|
+ integration tests, the literate quickstart, and the docs CI gate. It's
|
|
17
18
|
what GitHub Actions runs on every push and PR.
|
|
18
19
|
|
|
20
|
+
`git config core.hooksPath .githooks` activates the in-repo
|
|
21
|
+
[`.githooks/commit-msg`](.githooks/commit-msg) hook which rejects commit
|
|
22
|
+
messages containing `Co-Authored-By:` trailers, robot emojis, or other
|
|
23
|
+
AI-tool attribution patterns. The same check runs in CI (`lint-commits`
|
|
24
|
+
job) and as a pytest invariant (`tests/invariants/test_no_ai_signatures.py`)
|
|
25
|
+
— defense in depth.
|
|
26
|
+
|
|
19
27
|
## Workflow
|
|
20
28
|
|
|
21
29
|
1. Open an issue first for non-trivial changes — saves wasted work on both sides.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
@@ -41,10 +41,11 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
|
|
42
42
|
# agent-runner
|
|
43
43
|
|
|
44
|
-
A restart-on-exit supervisor for autonomous
|
|
45
|
-
Code
|
|
46
|
-
the
|
|
47
|
-
|
|
44
|
+
A restart-on-exit supervisor for autonomous coding CLIs. Tested with Claude
|
|
45
|
+
Code and aider out of the box; any prompt-arg CLI via custom config. Spawn
|
|
46
|
+
the agent round-after-round under defenses that prevent the failure modes
|
|
47
|
+
that bite in production: stuck rounds, orphan commits, OAuth burn loops,
|
|
48
|
+
full disks, runaway memory.
|
|
48
49
|
|
|
49
50
|
```
|
|
50
51
|
┌──────────────────────────────────────────┐
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
# agent-runner
|
|
6
6
|
|
|
7
|
-
A restart-on-exit supervisor for autonomous
|
|
8
|
-
Code
|
|
9
|
-
the
|
|
10
|
-
|
|
7
|
+
A restart-on-exit supervisor for autonomous coding CLIs. Tested with Claude
|
|
8
|
+
Code and aider out of the box; any prompt-arg CLI via custom config. Spawn
|
|
9
|
+
the agent round-after-round under defenses that prevent the failure modes
|
|
10
|
+
that bite in production: stuck rounds, orphan commits, OAuth burn loops,
|
|
11
|
+
full disks, runaway memory.
|
|
11
12
|
|
|
12
13
|
```
|
|
13
14
|
┌──────────────────────────────────────────┐
|
|
@@ -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.8'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 8)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""Agent subprocess management —
|
|
1
|
+
"""Agent subprocess management — spawns the configured agent CLI process.
|
|
2
2
|
|
|
3
3
|
Defenses encoded here:
|
|
4
4
|
- R725: SIGTERM handler reaps process group before runner exits
|
|
5
5
|
- R1128: ROUND_TIMEOUT is wall-clock hard wall (no activity-based extension)
|
|
6
6
|
- #307: start_new_session=True isolates subprocess in its own pgrp
|
|
7
|
-
- env injection:
|
|
7
|
+
- env injection: per-CLI envs come from AgentConfig.env (preset-supplied);
|
|
8
|
+
no implicit injection in this module.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
@@ -97,26 +98,14 @@ def run(
|
|
|
97
98
|
log_file.close()
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
CRITICAL_ENV_DEFAULTS: dict[str, str] = {
|
|
101
|
-
"DISABLE_AUTOUPDATER": "1", # do not let claude self-update mid-loop
|
|
102
|
-
"CLAUDE_CODE_EFFORT_LEVEL": "xhigh", # full effort, not default
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def merge_critical_envs(user_env: dict[str, str]) -> dict[str, str]:
|
|
107
|
-
"""Merge user env with CRITICAL_ENV_DEFAULTS — critical always wins."""
|
|
108
|
-
merged = dict(user_env)
|
|
109
|
-
merged.update(CRITICAL_ENV_DEFAULTS)
|
|
110
|
-
return merged
|
|
111
|
-
|
|
112
|
-
|
|
113
101
|
def install_sigterm_reaper(reaper: Callable[[], None]) -> object:
|
|
114
102
|
"""Install a SIGTERM handler that calls ``reaper()`` first.
|
|
115
103
|
|
|
116
|
-
R725 defense: when supervisor receives SIGTERM (e.g. systemctl stop,
|
|
117
|
-
kill), bash wrapper would otherwise respawn fresh runner
|
|
118
|
-
keeps running → two
|
|
119
|
-
swallow first commit's chat-room
|
|
104
|
+
R725 defense: when the supervisor receives SIGTERM (e.g. systemctl stop,
|
|
105
|
+
manual kill), the bash wrapper would otherwise respawn a fresh runner
|
|
106
|
+
while the old agent keeps running → two agent processes race on the same
|
|
107
|
+
git tree, the second commit can swallow the first commit's chat-room
|
|
108
|
+
entry. Reaper terminates pgroup first.
|
|
120
109
|
|
|
121
110
|
Returns the previous SIGTERM handler so caller can restore it.
|
|
122
111
|
"""
|
|
@@ -71,10 +71,16 @@ def _systemctl_user(*args: str) -> None:
|
|
|
71
71
|
# init / install / uninstall
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def init(
|
|
74
|
+
def init(
|
|
75
|
+
work_dir: Path | None = None,
|
|
76
|
+
*,
|
|
77
|
+
preset: str = "claude",
|
|
78
|
+
force: bool = False,
|
|
79
|
+
commit: bool = True,
|
|
80
|
+
) -> InitResult:
|
|
75
81
|
if work_dir is None:
|
|
76
82
|
work_dir = Path.cwd()
|
|
77
|
-
return scaffold_project(work_dir, force=force, commit=commit)
|
|
83
|
+
return scaffold_project(work_dir, preset=preset, force=force, commit=commit)
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
def install(
|
|
@@ -234,6 +240,9 @@ def _log_dir_for_project(project: str | Path) -> Path:
|
|
|
234
240
|
# for callers that only use lifecycle verbs.
|
|
235
241
|
|
|
236
242
|
from agent_runner import defenses, monitor # noqa: E402
|
|
243
|
+
from agent_runner.events import HOOK_FAILED # noqa: E402
|
|
244
|
+
|
|
245
|
+
_RECENT_HOOK_FAILURES_LIMIT = 10
|
|
237
246
|
|
|
238
247
|
|
|
239
248
|
def peek(
|
|
@@ -260,6 +269,16 @@ def peek(
|
|
|
260
269
|
if current is None:
|
|
261
270
|
raise KeyError(f"round {round_num} not found under {log_dir}/rounds/")
|
|
262
271
|
recent = parsed_events[-events:] if events else []
|
|
272
|
+
# Walk the tail in reverse so we stop as soon as the limit is filled.
|
|
273
|
+
# parsed_events grows unboundedly over a project's lifetime; a full-scan
|
|
274
|
+
# comprehension here would dominate watch-loop peek cost.
|
|
275
|
+
recent_hook_failures: list[dict[str, Any]] = []
|
|
276
|
+
for e in reversed(parsed_events):
|
|
277
|
+
if e.get("event") == HOOK_FAILED:
|
|
278
|
+
recent_hook_failures.append(e)
|
|
279
|
+
if len(recent_hook_failures) == _RECENT_HOOK_FAILURES_LIMIT:
|
|
280
|
+
break
|
|
281
|
+
recent_hook_failures.reverse()
|
|
263
282
|
|
|
264
283
|
state = ProjectState(
|
|
265
284
|
project=base_state.project,
|
|
@@ -280,6 +299,7 @@ def peek(
|
|
|
280
299
|
system=base_state.system,
|
|
281
300
|
service=status(project if project is not None else work_dir),
|
|
282
301
|
recent_events=recent,
|
|
302
|
+
recent_hook_failures=recent_hook_failures,
|
|
283
303
|
)
|
|
284
304
|
return state if select is None else select_path(state, select)
|
|
285
305
|
|
|
@@ -72,6 +72,7 @@ class ProjectState:
|
|
|
72
72
|
system: SystemMetrics
|
|
73
73
|
service: ServiceStatus
|
|
74
74
|
recent_events: list[dict[str, Any]] = field(default_factory=list)
|
|
75
|
+
recent_hook_failures: list[dict[str, Any]] = field(default_factory=list)
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
@dataclass(frozen=True)
|
|
@@ -130,6 +131,7 @@ class InitResult:
|
|
|
130
131
|
work_dir: Path
|
|
131
132
|
files_created: list[Path]
|
|
132
133
|
committed: bool
|
|
134
|
+
preset: str = "claude" # 0.1.7+; default for backward compat with synthesised InitResults
|
|
133
135
|
|
|
134
136
|
|
|
135
137
|
@dataclass(frozen=True)
|
|
@@ -12,10 +12,11 @@ from typing import Any
|
|
|
12
12
|
from agent_runner.api_types import ProjectState
|
|
13
13
|
from agent_runner.config import Config, load_config
|
|
14
14
|
from agent_runner.events import plugin_event_kinds
|
|
15
|
-
from agent_runner.hooks import plugin_context_enrichers
|
|
15
|
+
from agent_runner.hooks import plugin_context_enrichers, post_round_hooks, pre_round_hooks
|
|
16
16
|
from agent_runner.monitor import plugin_detectors
|
|
17
|
+
from agent_runner.vcs_state import plugin_owned_paths
|
|
17
18
|
|
|
18
|
-
PEEK_SCHEMA_VERSION = "1.
|
|
19
|
+
PEEK_SCHEMA_VERSION = "1.5"
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def cfg_from_args(args) -> Config:
|
|
@@ -48,7 +49,10 @@ def emit(value: Any, *, json_mode: bool) -> None:
|
|
|
48
49
|
"plugins": {
|
|
49
50
|
"event_kinds": plugin_event_kinds(),
|
|
50
51
|
"context_enrichers": plugin_context_enrichers(),
|
|
52
|
+
"pre_round_hooks": [h.name for h in pre_round_hooks()],
|
|
53
|
+
"post_round_hooks": [h.name for h in post_round_hooks()],
|
|
51
54
|
"detectors": plugin_detectors(),
|
|
55
|
+
"owned_paths": plugin_owned_paths(),
|
|
52
56
|
},
|
|
53
57
|
**_to_jsonable(value),
|
|
54
58
|
}
|
|
@@ -8,6 +8,12 @@ from agent_runner.cli.common import emit, fail, work_dir_from_args
|
|
|
8
8
|
|
|
9
9
|
def add_parser(sub, parent) -> None:
|
|
10
10
|
p = sub.add_parser("init", parents=[parent], help="Scaffold agent-runner project files")
|
|
11
|
+
p.add_argument(
|
|
12
|
+
"--preset",
|
|
13
|
+
choices=["claude", "aider"],
|
|
14
|
+
default="claude",
|
|
15
|
+
help="Which agent CLI preset to scaffold (default: claude)",
|
|
16
|
+
)
|
|
11
17
|
p.add_argument("--force", action="store_true", help="Overwrite existing toml")
|
|
12
18
|
g = p.add_mutually_exclusive_group()
|
|
13
19
|
g.add_argument(
|
|
@@ -24,8 +30,8 @@ def add_parser(sub, parent) -> None:
|
|
|
24
30
|
def cmd(args) -> int:
|
|
25
31
|
work_dir = work_dir_from_args(args)
|
|
26
32
|
try:
|
|
27
|
-
result = api.init(work_dir, force=args.force, commit=args.commit)
|
|
28
|
-
except (FileExistsError, RuntimeError) as e:
|
|
33
|
+
result = api.init(work_dir, preset=args.preset, force=args.force, commit=args.commit)
|
|
34
|
+
except (FileExistsError, RuntimeError, FileNotFoundError) as e:
|
|
29
35
|
return fail(str(e))
|
|
30
36
|
emit(result, json_mode=getattr(args, "json", False))
|
|
31
37
|
return 0
|
|
@@ -15,6 +15,7 @@ class AgentConfig:
|
|
|
15
15
|
command: list[str]
|
|
16
16
|
prompt_arg_template: list[str]
|
|
17
17
|
name: str | None = None
|
|
18
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@dataclass(frozen=True)
|
|
@@ -38,14 +39,16 @@ class VcsConfig:
|
|
|
38
39
|
stash_idempotency_s: int = 5
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
# Default auth-failure detection regex —
|
|
42
|
-
#
|
|
43
|
-
# providers override via [monitor].auth_fail_patterns.
|
|
42
|
+
# Default auth-failure detection regex — matches common OAuth/401/expired-session
|
|
43
|
+
# vocabularies. Presets override [monitor].auth_fail_hint per CLI.
|
|
44
44
|
_DEFAULT_AUTH_PATTERNS: list[str] = [
|
|
45
45
|
r"\b(oauth|unauthorized|401|api[_ ]key|"
|
|
46
46
|
r"auth(entication)?[_ -]?(failed|error|expired)|session.*expired)\b",
|
|
47
47
|
]
|
|
48
|
-
|
|
48
|
+
# Default auth-failure hint is empty — per-CLI hints come from preset files
|
|
49
|
+
# (agent_runner/presets/*.toml) which write `[monitor].auth_fail_hint` into the
|
|
50
|
+
# user's agent-runner.toml at scaffold time.
|
|
51
|
+
_DEFAULT_AUTH_HINT: str = ""
|
|
49
52
|
|
|
50
53
|
# Default allow-list of detector names whose ``stop_service`` action is honored.
|
|
51
54
|
# Plugin detectors must be added explicitly by the operator to opt them in.
|
|
@@ -94,6 +97,7 @@ def load_config(toml_path: Path) -> Config:
|
|
|
94
97
|
command=list(_require(agent_d, "command")),
|
|
95
98
|
prompt_arg_template=list(_require(agent_d, "prompt_arg_template")),
|
|
96
99
|
name=agent_d.get("name"),
|
|
100
|
+
env={str(k): str(v) for k, v in agent_d.get("env", {}).items()},
|
|
97
101
|
)
|
|
98
102
|
raw_work_dir = str(_require(raw, "runtime", "work_dir"))
|
|
99
103
|
work_dir = _expand_path(raw_work_dir, "").resolve()
|
|
@@ -13,7 +13,6 @@ from dataclasses import dataclass
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
-
from agent_runner.agent_runtime import CRITICAL_ENV_DEFAULTS
|
|
17
16
|
from agent_runner.config import Config
|
|
18
17
|
|
|
19
18
|
|
|
@@ -73,12 +72,13 @@ def catalog(cfg: Config) -> list[Defense]:
|
|
|
73
72
|
),
|
|
74
73
|
Defense(
|
|
75
74
|
name="critical_envs_injection",
|
|
76
|
-
value=
|
|
75
|
+
value=sorted(cfg.agent.env.keys()),
|
|
77
76
|
codifies=(
|
|
78
|
-
"
|
|
77
|
+
"Env injection via [agent.env] block — preset-supplied per CLI "
|
|
78
|
+
"(e.g. DISABLE_AUTOUPDATER for claude prevents mid-loop self-updates)"
|
|
79
79
|
),
|
|
80
80
|
guarded_by=None,
|
|
81
|
-
current_state="active",
|
|
81
|
+
current_state="active" if cfg.agent.env else "off",
|
|
82
82
|
),
|
|
83
83
|
Defense(
|
|
84
84
|
name="startup_smoke_check",
|
|
@@ -22,6 +22,11 @@ from datetime import UTC, datetime
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
+
# Cross-module event-kind constants. Most kinds are emitted in only one place
|
|
26
|
+
# (runner.py), but kinds that are also CONSUMED elsewhere (filtered, surfaced
|
|
27
|
+
# in peek, asserted in tests) earn a constant to keep the spelling honest.
|
|
28
|
+
HOOK_FAILED = "hook_failed"
|
|
29
|
+
|
|
25
30
|
_BUILTIN_KINDS: frozenset[str] = frozenset(
|
|
26
31
|
{
|
|
27
32
|
"round_start",
|
|
@@ -38,7 +43,7 @@ _BUILTIN_KINDS: frozenset[str] = frozenset(
|
|
|
38
43
|
"round_end",
|
|
39
44
|
"monitor_alert_emitted",
|
|
40
45
|
"monitor_auto_stop_triggered",
|
|
41
|
-
|
|
46
|
+
HOOK_FAILED,
|
|
42
47
|
}
|
|
43
48
|
)
|
|
44
49
|
|
|
@@ -28,7 +28,7 @@ from agent_runner.api_types import (
|
|
|
28
28
|
ServiceStatus,
|
|
29
29
|
SystemMetrics,
|
|
30
30
|
)
|
|
31
|
-
from agent_runner.config import
|
|
31
|
+
from agent_runner.config import _DEFAULT_AUTH_PATTERNS
|
|
32
32
|
from agent_runner.context_store import read_json
|
|
33
33
|
from agent_runner.events import emit as emit_event
|
|
34
34
|
from agent_runner.events import now_iso_ms, parse_iso_ms
|
|
@@ -278,7 +278,7 @@ def detect_oauth_fail(
|
|
|
278
278
|
"matches": matches,
|
|
279
279
|
"window": total,
|
|
280
280
|
"threshold": threshold,
|
|
281
|
-
"hint": hint if hint is not None else
|
|
281
|
+
"hint": hint if hint is not None else "",
|
|
282
282
|
},
|
|
283
283
|
auto_action="stop_service",
|
|
284
284
|
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# agent-runner.toml — generated by `agent-runner init --preset aider`.
|
|
2
|
+
#
|
|
3
|
+
# Prereqs:
|
|
4
|
+
# - aider installed (`pipx install aider-chat`)
|
|
5
|
+
# - your model provider env var set (OPENAI_API_KEY / ANTHROPIC_API_KEY /
|
|
6
|
+
# DEEPSEEK_API_KEY / OPENROUTER_API_KEY / etc.) — `aider --models` lists
|
|
7
|
+
# detected providers.
|
|
8
|
+
|
|
9
|
+
[agent]
|
|
10
|
+
command = ["aider", "--yes-always", "--no-stream", "--analytics-disable"]
|
|
11
|
+
prompt_arg_template = ["--message", "{prompt}"]
|
|
12
|
+
name = "aider"
|
|
13
|
+
# [agent.env] omitted — aider needs no env injection.
|
|
14
|
+
|
|
15
|
+
[runtime]
|
|
16
|
+
work_dir = "."
|
|
17
|
+
log_dir = "~/.agent-runner/{project}/logs"
|
|
18
|
+
round_timeout_s = 1800
|
|
19
|
+
restart_delay_s = 3
|
|
20
|
+
|
|
21
|
+
[prompt]
|
|
22
|
+
file = "./prompts/main.md"
|
|
23
|
+
inject_context = true
|
|
24
|
+
|
|
25
|
+
[vcs]
|
|
26
|
+
orphan_action = "stash"
|
|
27
|
+
stash_idempotency_s = 5
|
|
28
|
+
|
|
29
|
+
[monitor]
|
|
30
|
+
auth_fail_hint = "Verify your model provider env var (OPENAI_API_KEY / ANTHROPIC_API_KEY / DEEPSEEK_API_KEY / etc.); run `aider --models` to list detected providers."
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# agent-runner.toml — generated by `agent-runner init --preset claude`.
|
|
2
|
+
|
|
3
|
+
[agent]
|
|
4
|
+
command = ["claude", "--model", "claude-opus-4-7",
|
|
5
|
+
"--dangerously-skip-permissions",
|
|
6
|
+
"--verbose", "--output-format", "stream-json"]
|
|
7
|
+
prompt_arg_template = ["-p", "{prompt}"]
|
|
8
|
+
name = "claude"
|
|
9
|
+
|
|
10
|
+
[agent.env]
|
|
11
|
+
DISABLE_AUTOUPDATER = "1"
|
|
12
|
+
CLAUDE_CODE_EFFORT_LEVEL = "xhigh"
|
|
13
|
+
|
|
14
|
+
[runtime]
|
|
15
|
+
work_dir = "."
|
|
16
|
+
log_dir = "~/.agent-runner/{project}/logs"
|
|
17
|
+
round_timeout_s = 1800
|
|
18
|
+
restart_delay_s = 3
|
|
19
|
+
|
|
20
|
+
[prompt]
|
|
21
|
+
file = "./prompts/main.md"
|
|
22
|
+
inject_context = true
|
|
23
|
+
|
|
24
|
+
[vcs]
|
|
25
|
+
orphan_action = "stash"
|
|
26
|
+
stash_idempotency_s = 5
|
|
27
|
+
|
|
28
|
+
[monitor]
|
|
29
|
+
auth_fail_hint = "Run `claude /login` on the supervisor host or refresh ANTHROPIC_API_KEY."
|