coderio 0.4.1__tar.gz → 0.4.2__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.
- {coderio-0.4.1 → coderio-0.4.2}/CHANGELOG.md +100 -2
- {coderio-0.4.1 → coderio-0.4.2}/PKG-INFO +17 -5
- {coderio-0.4.1 → coderio-0.4.2}/README.md +17 -5
- {coderio-0.4.1 → coderio-0.4.2}/README_en.md +16 -4
- {coderio-0.4.1 → coderio-0.4.2}/pyproject.toml +1 -1
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/_deepagents_compat.py +39 -10
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/deep_loop.py +40 -6
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/hooks.py +75 -15
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/app.py +18 -3
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/run_cmd.py +111 -27
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/config/loader.py +20 -8
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/config/models.py +7 -1
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/config/trust.py +69 -15
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/models.py +9 -3
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/sandbox_runner.py +11 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_deepagents_compat.py +80 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_hooks.py +116 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_run_cmd.py +112 -5
- {coderio-0.4.1 → coderio-0.4.2}/tests/config/test_trust.py +79 -0
- {coderio-0.4.1 → coderio-0.4.2}/uv.lock +1 -1
- {coderio-0.4.1 → coderio-0.4.2}/.github/dependabot.yml +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/.github/workflows/ci.yml +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/.github/workflows/release.yml +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/.gitignore +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/CONTRIBUTING.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/LICENSE +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/THIRD_PARTY_LICENSES.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/docs/coderio-architecture.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/scripts/verify_deepagent_live.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/scripts/verify_harness_live.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/command_review.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/harness.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/harness_middleware.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/permission_middleware.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/prompts.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/skill_tool.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/state.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/agent/stream.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/commands.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/credentials.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/mcp_cmd.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/multimodal.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/onboarding.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/providers.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/render.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/repl.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/skills_cmd.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/stream.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/tui.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/tui_onboarding.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/tui_screens.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/cli/tui_widgets.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/config/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/config/bootstrap.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/llm/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/llm/factory.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/llm/probe.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/mcp_loader.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/session/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/session/message.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/session/store.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/bundled/executing-plans/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/LICENSE +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/README.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/code-review/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/code-review/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/commit-message/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/commit-message/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/debugging/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/debugging/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/error-handling/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/error-handling/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/naming/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/naming/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/spec-writing/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/testing/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/testing/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/parser.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/skills/store.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/base.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/bash.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/command_policy.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/edit_file.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/glob_tool.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/grep_tool.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/linux_sandbox.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/list_dir.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/multi_edit.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/note.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/permission.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/read_file.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/todo.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/web_fetch.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/web_search.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/win_job.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/win_sandbox.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/src/coderio/tools/write_file.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/conftest.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_command_review.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_deep_integration.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_deep_loop.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_deep_loop_unit.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_harness.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_harness_middleware.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_perf_baseline.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_prompts.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_skill_tool.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_state.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/agent/test_stream.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_app.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_command_menu.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_commands.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_credentials.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_input_isolation.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_live_thinking.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_mcp_cmd.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_multimodal.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_onboarding.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_onboarding_logic.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_providers.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_render.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_repl_loop.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_repl_reset.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_skills_cmd.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_status_bar.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_stream.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_truncation.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_tui_onboarding.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_tui_picker.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_tui_screens.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_tui_startup.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_tui_widgets.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/cli/test_ux_features.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/config/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/config/test_bootstrap.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/config/test_loader.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/config/test_models.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/conftest.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/e2e/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/llm/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/llm/test_factory.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/llm/test_probe.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/session/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/session/test_message.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/session/test_store.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/skills/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/skills/test_bundled.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/skills/test_parser.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/skills/test_skill_tools.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/skills/test_store.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/test_mcp_loader.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/test_p0_stability.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/test_repl.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/test_version.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/__init__.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_bash.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_command_policy.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_concrete_gates.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_edit_prefix.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_file_tools.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_glob_tool.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_grep_tool.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_list_dir.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_multi_edit.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_note.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_permission.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_registry.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_sandbox.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_search_tools.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_todo.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_web.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_web_fetch.py +0 -0
- {coderio-0.4.1 → coderio-0.4.2}/tests/tools/test_win_job.py +0 -0
|
@@ -6,6 +6,106 @@ All notable changes to coderio are documented here. The format follows
|
|
|
6
6
|
`pyproject.toml`'s `[project].version` — `coderio.__version__` reads it via
|
|
7
7
|
`importlib.metadata`.
|
|
8
8
|
|
|
9
|
+
## [0.4.2] — 2026-08-18
|
|
10
|
+
|
|
11
|
+
### Security — v3 audit short-term batch (#7/#8/#9/#11/#14, all runtime-verified)
|
|
12
|
+
- **#7 · headless default permission full → plan**: a headless entry that
|
|
13
|
+
silently allowed everything was a zero-confirmation door. `coderio run` now
|
|
14
|
+
defaults to read-only plan; full requires the explicit
|
|
15
|
+
`--dangerously-skip-permissions` flag (Claude Code's name for the same
|
|
16
|
+
escape hatch). confirm/auto_edit fail fast without a TTY (the gate is lazy,
|
|
17
|
+
so input() would otherwise EOFError mid-execution); invalid --permission
|
|
18
|
+
values are rejected up front.
|
|
19
|
+
- **#8 · project-layer skills join the trust gate**: a repo shipping ONLY
|
|
20
|
+
`.coderio/skills` previously loaded them with zero confirmation — yet skills
|
|
21
|
+
enter the system prompt and may carry tools.py that exec's on activation.
|
|
22
|
+
Skills now participate in discovery + content fingerprint (per-file hash;
|
|
23
|
+
any skill edit re-triggers the prompt), and the confirmation summary marks
|
|
24
|
+
skills carrying tools.py with "⚠ executes code". tools.py load failures are
|
|
25
|
+
logged, no longer silent.
|
|
26
|
+
- **#9 · trust store hardening**: the store gets owner-only permissions
|
|
27
|
+
(POSIX 0600 / Windows icacls, reusing the credentials helper); a corrupt
|
|
28
|
+
store is left untouched instead of being reset (the old reset destroyed
|
|
29
|
+
every other repo's trust entries); `SandboxFsConfig.deny_write` now
|
|
30
|
+
defaults to `["~/.coderio"]` so sandboxed commands can't rewrite the
|
|
31
|
+
config/credentials/trust store (explicit `deny_write = []` opts out;
|
|
32
|
+
Linux bwrap only — Windows sandbox is a no-op today).
|
|
33
|
+
- **#11 · research subagent execution-time enforcement**: PermissionMiddleware
|
|
34
|
+
+ CommandReviewMiddleware now ride the research subagent (the tool
|
|
35
|
+
whitelist filters what the model SEES; these gate what actually RUNS).
|
|
36
|
+
Failover direction reversed: a whitelist that cannot be constructed — or
|
|
37
|
+
fails at runtime — degrades to a DENY-ALL middleware (zero tools), not to
|
|
38
|
+
the old no-middleware state that inherited every tool including
|
|
39
|
+
write/execute.
|
|
40
|
+
- **#14 · headless wall-clock timeout + exit codes**: `--timeout <seconds>`
|
|
41
|
+
(thread+join; SIGALRM doesn't exist on Windows) exits 124 on expiry;
|
|
42
|
+
agent execution failures exit 2 (distinct from config errors' 1); success
|
|
43
|
+
0. Documented in README for CI use.
|
|
44
|
+
|
|
45
|
+
### Fixed — hooks completion (2026-08-14 v3 audit P1/P2 items)
|
|
46
|
+
- **Timeout latency (v3 P1: timeout=2 hook took 12s)**: the post-kill pipe
|
|
47
|
+
drain waited 10s for an EOF that never comes on Windows (a pre-kill
|
|
48
|
+
grandchild holds the write end) — and the drained output was never consumed
|
|
49
|
+
anyway (hardcoded empty return). Now a 1s grace then abandon: a timeout=2
|
|
50
|
+
hook returns in ~3s. Windows grandchild leak documented as a known
|
|
51
|
+
limitation (the turn is no longer hostage; root fix = CREATE_SUSPENDED +
|
|
52
|
+
pre-assigned Job + stdin pipe through _create_process_with_token). Latency
|
|
53
|
+
regression test added (`elapsed < timeout + 2`), mutation-verified.
|
|
54
|
+
- **Per-event budget (v3 P1: N hooks × 60s on every tool call)**: fire() now
|
|
55
|
+
enforces a 30s total budget per event (overridable on HookRunner for
|
|
56
|
+
tests). Each hook's timeout tightens to min(spec.timeout, budget
|
|
57
|
+
remaining); exhaustion skips the remaining hooks with an error note
|
|
58
|
+
(fail-open, consistent with the module's positioning).
|
|
59
|
+
- **Subagents bypassed hooks (v3 #12)**: research and general-purpose
|
|
60
|
+
subagents both carry HooksMiddleware now (outermost, same order as the main
|
|
61
|
+
agent) — task() delegation can no longer sidestep PreToolUse/PostToolUse.
|
|
62
|
+
No middleware overhead when no hooks are configured.
|
|
63
|
+
- **Repo [[hooks]] silently dropped user hooks (v3 P2)**: _merge replaces
|
|
64
|
+
lists wholesale, so a repo's hooks config wiped the user's protective
|
|
65
|
+
hooks. Hooks now APPEND across layers — user hooks first (first-blocker-
|
|
66
|
+
wins: the user's deny reason is what the model sees). The first and only
|
|
67
|
+
list-merge in the loader; every other key keeps replace semantics.
|
|
68
|
+
- +7 tests (latency guard, budget skip/tighten, subagent middleware stacks ×3,
|
|
69
|
+
merge order). Mutation check: reintroducing the 10s drain turns the latency
|
|
70
|
+
test red (10.1s ≥ 4s bound) — the guard actually guards.
|
|
71
|
+
|
|
72
|
+
### Fixed — 2026-08-18 self-audit (3 bugs + 3 warnings, all runtime-verified)
|
|
73
|
+
- **BUG A · `--permission full` had no flag gate (claim vs code mismatch)**:
|
|
74
|
+
the 0.4.1 commit message and CHANGELOG claimed "full requires
|
|
75
|
+
--dangerously-skip-permissions" — no such gate existed; `--permission full`
|
|
76
|
+
alone ran with full access (verified end-to-end by an audit agent that
|
|
77
|
+
unintentionally ran three real model turns through it). The gate is real
|
|
78
|
+
now: full without the flag exits 1. +2 tests (negative and positive sides).
|
|
79
|
+
Fourth seam-class failure in project history (v1: deepagents↔coderio; v3:
|
|
80
|
+
config↔runtime, trust↔mcp_loader; this: message↔code) — new rule in the
|
|
81
|
+
audit report: every behavior claim in a commit message must have a
|
|
82
|
+
corresponding verified command output before release.
|
|
83
|
+
- **BUG B · Windows NUL bypassed the confirm-mode TTY check**: `</dev/null`
|
|
84
|
+
(the classic CI idiom) makes `sys.stdin.isatty()` return True on Windows
|
|
85
|
+
(MSVCRT treats character devices as ttys) — the check passed, then
|
|
86
|
+
`input()` read NUL, EOFError'd, and crashed the agent mid-execution.
|
|
87
|
+
confirm/auto_edit are no longer valid headless values AT ALL (rejected
|
|
88
|
+
unconditionally); the interactive TUI is the only prompting surface.
|
|
89
|
+
- **BUG C · the deny_write default was dead config for most users**:
|
|
90
|
+
SandboxFsConfig's `deny_write=["~/.coderio"]` default only applied when the
|
|
91
|
+
`[tools.sandbox_filesystem]` table EXISTED — users without the table got
|
|
92
|
+
fs_config=None, and bwrap's built-in layout had no deny_write, so sandboxed
|
|
93
|
+
commands could write ~/.coderio (config/credentials/TRUST STORE). The
|
|
94
|
+
sandbox runner now constructs the default config when fs_config is None;
|
|
95
|
+
explicit user configs (including `deny_write = []`) pass through unchanged.
|
|
96
|
+
- **hooks budget skip-count was dead code**: `self.specs[len(self.specs):]`
|
|
97
|
+
is always the empty slice, so the "N hook(s) skipped" message always
|
|
98
|
+
printed "remaining" — the count never appeared (and no test asserted it).
|
|
99
|
+
Fixed with enumerate-indexed slicing (verified: 4 hooks skipped →
|
|
100
|
+
"4 hook(s) skipped").
|
|
101
|
+
- **trust store `null` slipped the corruption guard**: JSON `null` parsed to
|
|
102
|
+
None and bypassed the `is not None` shape check, getting overwritten. All
|
|
103
|
+
non-dict JSON (including null) now leaves the store untouched.
|
|
104
|
+
- **nested typer.Exit produced a misleading second error line**: typer.Exit
|
|
105
|
+
is a RuntimeError subclass, so the session-load failure's clean exit was
|
|
106
|
+
re-caught by "Runtime setup failed" and printed twice. Nested exits now
|
|
107
|
+
pass through with their original code and message.
|
|
108
|
+
|
|
9
109
|
## [0.4.1] — 2026-08-17
|
|
10
110
|
|
|
11
111
|
### Added
|
|
@@ -67,8 +167,6 @@ All notable changes to coderio are documented here. The format follows
|
|
|
67
167
|
solely on the token stream; a non-streamed final message vanished. The
|
|
68
168
|
final result is now always printed (after a separator in non-quiet mode).
|
|
69
169
|
|
|
70
|
-
## [Unreleased]
|
|
71
|
-
|
|
72
170
|
## [0.4.0] — 2026-08-17
|
|
73
171
|
|
|
74
172
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: coderio
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A skill-driven coding agent — structural harness, foldable thinking TUI, deepagents engine.
|
|
5
5
|
Author: coderio contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,7 +81,7 @@ Core philosophy: **skills are the playbook, the harness is the discipline, tools
|
|
|
81
81
|
- **SSRF-protected web_fetch**: scheme allowlist (http/https only), private/loopback/link-local IP blocking (incl. cloud metadata endpoints), per-hop redirect validation, and a 1 MB response cap.
|
|
82
82
|
- **Multi-layer OS sandbox** (Linux real isolation): `sandbox_mode = "job" | "write"` — Job Object resource limits on Windows; bubblewrap namespace isolation on Linux (read-only root, read-write workspace, optional network cutoff, Claude-Code-compatible filesystem 4-tuple `allow_write`/`deny_write`/`deny_read`/`allow_read`).
|
|
83
83
|
- **MCP support**: connect external MCP servers via `.mcp.json` (Claude Code-compatible format); stdio and HTTP transports; project-level config overrides user-level. `coderio mcp` CLI manages entries; MCP tools are gated by the permission system like built-in tools.
|
|
84
|
-
- **Repo-config trust confirmation**: first time a repository's `.coderio/config.toml
|
|
84
|
+
- **Repo-config trust confirmation**: first time a repository's `.coderio/config.toml`, `.mcp.json`, **or project-layer skills** (with tools.py markers) is seen, coderio shows what it contains (permission mode, base_url, hook/MCP commands) and requires explicit confirmation — content-hashed, so upstream edits re-trigger the prompt. Cloning a hostile repo no longer means silent arbitrary code execution.
|
|
85
85
|
- **Multiple providers + named profiles**: Zhipu GLM / StepFun coding plans (Anthropic protocol) + OpenAI-compatible endpoints; multiple profiles with runtime `/profile` switching.
|
|
86
86
|
- **Headless mode**: `coderio run "task"` for one-shot, non-interactive runs (CI, scripting, benchmark harnesses).
|
|
87
87
|
|
|
@@ -314,7 +314,15 @@ command = "cat .hooks/conventions.txt"
|
|
|
314
314
|
|
|
315
315
|
**Honest positioning**: hooks are an EXTENSIBILITY point, not a security boundary — timeout/crash/non-2 exits all fail-open (a broken hook must never brick the agent loop). Hard policy belongs to the permission gate + command blacklist. Repo-level hooks live in config.toml and ride the existing repo-config trust gate (cloning a hostile repo does not silently run its hooks).
|
|
316
316
|
|
|
317
|
-
**
|
|
317
|
+
**Execution semantics**:
|
|
318
|
+
- **Timeouts**: 60s per hook by default (`timeout` configurable); **30s per-event total budget** — when all matching hooks together exceed it, the rest are skipped (fail-open); one slow hook can't eat the whole turn
|
|
319
|
+
- **Serial execution**: multiple hooks run in config order; the first blocker's reason wins, but every hook still runs (side effects preserved)
|
|
320
|
+
- **Environment inheritance**: hooks inherit the full environment (including API keys) — only configure hook commands you trust
|
|
321
|
+
- **Subagents are covered too**: research / general-purpose subagents both carry HooksMiddleware — `task()` delegation cannot bypass PreToolUse
|
|
322
|
+
- **Merge semantics**: user-level `[[hooks]]` + project-level `[[hooks]]` **append** (user first — under first-blocker-wins, the user's protective hook's reason takes priority); a repo's config never drops the user's protections
|
|
323
|
+
- Windows grandchild leak on timeout is a known limitation (the turn is no longer hostage; the root fix awaits the CREATE_SUSPENDED approach)
|
|
324
|
+
|
|
325
|
+
**v1 event set and non-goals**: SessionStart (once per session, resume included) / UserPromptSubmit (reject or inject) / PreToolUse (deny) / PostToolUse (append feedback) / Stop (notification-only — never fights the harness force-continue). Not in v1: SessionEnd (no injection point), Notification/SubagentStop/PreCompact, `updatedInput` rewriting, hook-allow skipping the permission prompt.
|
|
318
326
|
|
|
319
327
|
Supported providers:
|
|
320
328
|
| provider_id | Description | Protocol |
|
|
@@ -343,9 +351,13 @@ coderio
|
|
|
343
351
|
coderio --provider bigmodel_coding_plan --model glm-5.2
|
|
344
352
|
|
|
345
353
|
# Headless one-shot run (CI / scripting / benchmarks)
|
|
346
|
-
coderio run "write a hello-world script and test it"
|
|
347
|
-
coderio run "fix the failing test in tests/foo.py" --quiet
|
|
354
|
+
coderio run "write a hello-world script and test it" # default: plan (read-only)
|
|
355
|
+
coderio run "fix the failing test in tests/foo.py" --quiet # final result only
|
|
348
356
|
coderio run "continue" --session-id <id> # resume a session
|
|
357
|
+
coderio run "run any command" --dangerously-skip-permissions # full access (explicit opt-in)
|
|
358
|
+
coderio run "task" --timeout 600 # wall-clock timeout (recommended for CI)
|
|
359
|
+
|
|
360
|
+
# headless exit codes: 0 success / 1 config error / 2 agent failure / 124 timeout
|
|
349
361
|
|
|
350
362
|
# Manage skills (install pulls from GitHub; needs git on PATH)
|
|
351
363
|
coderio skills list
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
- **命令审查层**:shell(execute)命令不受 virtual_mode 约束,所以额外加了一层 `CommandReviewMiddleware`——内置黑名单挡住 `rm -rf /`、`mkfs`、fork bomb、`dd of=/dev/`、shutdown 等破坏性命令(即使 FULL 模式也挡),用户可在 config.toml 追加 `blocked_commands`。这不是真 OS 沙箱(混淆命令可绕过正则),但能挡住绝大多数意外破坏。`network_allowed = false` 可完全禁用 web 工具(离线模式)
|
|
37
37
|
- **多 provider + 命名 profile**:智谱 GLM / 阶跃 StepFun 的 coding plan(Anthropic 协议)+ OpenAI 兼容;支持多套配置 profile,`/profile` 运行时切换
|
|
38
38
|
- **MCP 支持**:通过 `.mcp.json`(与 Claude Code 格式兼容)接入外部 MCP 服务器,自动加载它们的工具。支持 stdio(本地进程)和 HTTP(远程)两种传输。项目级 `.mcp.json` 覆盖用户级同名服务器
|
|
39
|
-
- **仓库配置信任确认**:首次在某仓库检测到 `.coderio/config.toml
|
|
39
|
+
- **仓库配置信任确认**:首次在某仓库检测到 `.coderio/config.toml`、`.mcp.json` **或项目级 skills**(含 tools.py 标注)时,展示其内容(权限模式、base_url、hook/MCP 命令)并要求显式确认——按内容 hash 记忆,上游改配置会重新触发确认。克隆恶意仓库不再等于静默任意代码执行
|
|
40
40
|
- **web_fetch SSRF 防护**:scheme 白名单(仅 http/https)+ 私网/环回/链路本地 IP 拦截(含云元数据端点)+ 逐跳重定向校验 + 1MB 响应上限
|
|
41
41
|
- **headless 模式**:`coderio run "任务"` 单次无交互运行(CI / 脚本 / benchmark 接入)
|
|
42
42
|
|
|
@@ -269,7 +269,15 @@ command = "cat .hooks/conventions.txt"
|
|
|
269
269
|
|
|
270
270
|
**诚实定位**:hooks 是**扩展点不是安全边界**——超时/崩溃/非 2 退出码都 fail-open(一个坏 hook 绝不能搞死 agent loop)。硬策略请用权限审批 + 命令黑名单。项目级 hooks 写在 config.toml 里,自动被仓库信任门覆盖(克隆恶意仓库不会静默执行其 hooks)。
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
**执行语义**:
|
|
273
|
+
- **超时**:每个 hook 默认 60s(`timeout` 可配);**单事件总预算 30s**——同事件所有 hook 合计超预算后,剩余 hook 跳过(fail-open),一个慢 hook 吃不光整个 turn
|
|
274
|
+
- **串行执行**:多 hook 按配置顺序串行,第一个 blocker 的理由生效,但所有 hook 都会执行(副作用保留)
|
|
275
|
+
- **环境继承**:hook 继承完整环境变量(含 API key)——只配置你信任的 hook 命令
|
|
276
|
+
- **子 agent 同样受约束**:research / general-purpose 子 agent 都挂 HooksMiddleware,`task()` 委派无法绕过 PreToolUse
|
|
277
|
+
- **合并语义**:用户级 `[[hooks]]` + 项目级 `[[hooks]]` **追加合并**(用户在前——首个 blocker 的优先理由来自用户的防护 hook),项目配置不会剔除用户防护
|
|
278
|
+
- Windows 孙进程泄漏为已知 limitation(超时后 turn 不受挟持;根治待 CREATE_SUSPENDED 方案)
|
|
279
|
+
|
|
280
|
+
**v1 事件集与不做的事**:SessionStart(每 session 一次,含 resume)/ UserPromptSubmit(可拒绝可注入)/ PreToolUse(可 deny)/ PostToolUse(追加反馈)/ Stop(通知型,不与 harness 续跑冲突)。不做:SessionEnd(无注入点)、Notification/SubagentStop/PreCompact、`updatedInput` 参数改写、hook allow 跳过权限弹窗。
|
|
273
281
|
|
|
274
282
|
支持的 provider:
|
|
275
283
|
| provider_id | 说明 | 协议 |
|
|
@@ -298,9 +306,13 @@ coderio
|
|
|
298
306
|
coderio --provider bigmodel_coding_plan --model glm-5.2
|
|
299
307
|
|
|
300
308
|
# headless 单次运行(CI / 脚本 / benchmark 接入)
|
|
301
|
-
coderio run "写一个 hello-world 脚本并测试"
|
|
302
|
-
coderio run "修复 tests/foo.py 里失败的测试" --quiet
|
|
303
|
-
coderio run "继续" --session-id <id>
|
|
309
|
+
coderio run "写一个 hello-world 脚本并测试" # 默认 plan(只读)
|
|
310
|
+
coderio run "修复 tests/foo.py 里失败的测试" --quiet # 只输出最终结果
|
|
311
|
+
coderio run "继续" --session-id <id> # 续跑会话
|
|
312
|
+
coderio run "跑任意命令" --dangerously-skip-permissions # full 权限(显式选择加入)
|
|
313
|
+
coderio run "任务" --timeout 600 # 墙钟超时(CI 建议)
|
|
314
|
+
|
|
315
|
+
# headless 退出码:0 成功 / 1 配置错误 / 2 agent 执行失败 / 124 超时
|
|
304
316
|
|
|
305
317
|
# 管理 skill(install 从 GitHub 拉取,需要 git 在 PATH)
|
|
306
318
|
coderio skills list
|
|
@@ -36,7 +36,7 @@ Core philosophy: **skills are the playbook, the harness is the discipline, tools
|
|
|
36
36
|
- **SSRF-protected web_fetch**: scheme allowlist (http/https only), private/loopback/link-local IP blocking (incl. cloud metadata endpoints), per-hop redirect validation, and a 1 MB response cap.
|
|
37
37
|
- **Multi-layer OS sandbox** (Linux real isolation): `sandbox_mode = "job" | "write"` — Job Object resource limits on Windows; bubblewrap namespace isolation on Linux (read-only root, read-write workspace, optional network cutoff, Claude-Code-compatible filesystem 4-tuple `allow_write`/`deny_write`/`deny_read`/`allow_read`).
|
|
38
38
|
- **MCP support**: connect external MCP servers via `.mcp.json` (Claude Code-compatible format); stdio and HTTP transports; project-level config overrides user-level. `coderio mcp` CLI manages entries; MCP tools are gated by the permission system like built-in tools.
|
|
39
|
-
- **Repo-config trust confirmation**: first time a repository's `.coderio/config.toml
|
|
39
|
+
- **Repo-config trust confirmation**: first time a repository's `.coderio/config.toml`, `.mcp.json`, **or project-layer skills** (with tools.py markers) is seen, coderio shows what it contains (permission mode, base_url, hook/MCP commands) and requires explicit confirmation — content-hashed, so upstream edits re-trigger the prompt. Cloning a hostile repo no longer means silent arbitrary code execution.
|
|
40
40
|
- **Multiple providers + named profiles**: Zhipu GLM / StepFun coding plans (Anthropic protocol) + OpenAI-compatible endpoints; multiple profiles with runtime `/profile` switching.
|
|
41
41
|
- **Headless mode**: `coderio run "task"` for one-shot, non-interactive runs (CI, scripting, benchmark harnesses).
|
|
42
42
|
|
|
@@ -269,7 +269,15 @@ command = "cat .hooks/conventions.txt"
|
|
|
269
269
|
|
|
270
270
|
**Honest positioning**: hooks are an EXTENSIBILITY point, not a security boundary — timeout/crash/non-2 exits all fail-open (a broken hook must never brick the agent loop). Hard policy belongs to the permission gate + command blacklist. Repo-level hooks live in config.toml and ride the existing repo-config trust gate (cloning a hostile repo does not silently run its hooks).
|
|
271
271
|
|
|
272
|
-
**
|
|
272
|
+
**Execution semantics**:
|
|
273
|
+
- **Timeouts**: 60s per hook by default (`timeout` configurable); **30s per-event total budget** — when all matching hooks together exceed it, the rest are skipped (fail-open); one slow hook can't eat the whole turn
|
|
274
|
+
- **Serial execution**: multiple hooks run in config order; the first blocker's reason wins, but every hook still runs (side effects preserved)
|
|
275
|
+
- **Environment inheritance**: hooks inherit the full environment (including API keys) — only configure hook commands you trust
|
|
276
|
+
- **Subagents are covered too**: research / general-purpose subagents both carry HooksMiddleware — `task()` delegation cannot bypass PreToolUse
|
|
277
|
+
- **Merge semantics**: user-level `[[hooks]]` + project-level `[[hooks]]` **append** (user first — under first-blocker-wins, the user's protective hook's reason takes priority); a repo's config never drops the user's protections
|
|
278
|
+
- Windows grandchild leak on timeout is a known limitation (the turn is no longer hostage; the root fix awaits the CREATE_SUSPENDED approach)
|
|
279
|
+
|
|
280
|
+
**v1 event set and non-goals**: SessionStart (once per session, resume included) / UserPromptSubmit (reject or inject) / PreToolUse (deny) / PostToolUse (append feedback) / Stop (notification-only — never fights the harness force-continue). Not in v1: SessionEnd (no injection point), Notification/SubagentStop/PreCompact, `updatedInput` rewriting, hook-allow skipping the permission prompt.
|
|
273
281
|
|
|
274
282
|
Supported providers:
|
|
275
283
|
| provider_id | Description | Protocol |
|
|
@@ -298,9 +306,13 @@ coderio
|
|
|
298
306
|
coderio --provider bigmodel_coding_plan --model glm-5.2
|
|
299
307
|
|
|
300
308
|
# Headless one-shot run (CI / scripting / benchmarks)
|
|
301
|
-
coderio run "write a hello-world script and test it"
|
|
302
|
-
coderio run "fix the failing test in tests/foo.py" --quiet
|
|
309
|
+
coderio run "write a hello-world script and test it" # default: plan (read-only)
|
|
310
|
+
coderio run "fix the failing test in tests/foo.py" --quiet # final result only
|
|
303
311
|
coderio run "continue" --session-id <id> # resume a session
|
|
312
|
+
coderio run "run any command" --dangerously-skip-permissions # full access (explicit opt-in)
|
|
313
|
+
coderio run "task" --timeout 600 # wall-clock timeout (recommended for CI)
|
|
314
|
+
|
|
315
|
+
# headless exit codes: 0 success / 1 config error / 2 agent failure / 124 timeout
|
|
304
316
|
|
|
305
317
|
# Manage skills (install pulls from GitHub; needs git on PATH)
|
|
306
318
|
coderio skills list
|
|
@@ -97,22 +97,48 @@ class _ToolWhitelistMiddleware(AgentMiddleware):
|
|
|
97
97
|
|
|
98
98
|
Wraps the same ModelRequest.override + filter mechanism as
|
|
99
99
|
``_ToolExclusionMiddleware`` but inverts the predicate.
|
|
100
|
+
|
|
101
|
+
Runtime failures degrade to an EMPTY tool set (deny-all), never to the
|
|
102
|
+
unfiltered original — a broken research subagent is preferable to a
|
|
103
|
+
research subagent that can suddenly see write/execute tools (v3 audit #11
|
|
104
|
+
reversed the old fail-open direction).
|
|
100
105
|
"""
|
|
101
106
|
|
|
102
107
|
def __init__(self, *, allowed: frozenset[str]) -> None:
|
|
103
108
|
self._allowed = allowed
|
|
104
109
|
|
|
105
110
|
def wrap_model_call(self, request, handler):
|
|
106
|
-
|
|
111
|
+
try:
|
|
107
112
|
filtered = [t for t in request.tools if _tool_name(t) in self._allowed]
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
return handler(request.override(tools=filtered))
|
|
114
|
+
except Exception as e: # noqa: BLE001 — degrade to deny-all, never fail-open
|
|
115
|
+
_log.warning("tool whitelist failed at runtime (degrading to NO tools): %s", e)
|
|
116
|
+
return handler(request.override(tools=[]))
|
|
110
117
|
|
|
111
118
|
async def awrap_model_call(self, request, handler):
|
|
112
|
-
|
|
119
|
+
try:
|
|
113
120
|
filtered = [t for t in request.tools if _tool_name(t) in self._allowed]
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
return await handler(request.override(tools=filtered))
|
|
122
|
+
except Exception as e: # noqa: BLE001 — degrade to deny-all, never fail-open
|
|
123
|
+
_log.warning("tool whitelist failed at runtime (degrading to NO tools): %s", e)
|
|
124
|
+
return await handler(request.override(tools=[]))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _DenyAllToolsMiddleware(AgentMiddleware):
|
|
128
|
+
"""Last-resort middleware: the model sees ZERO tools.
|
|
129
|
+
|
|
130
|
+
Returned when the whitelist middleware itself cannot be constructed — the
|
|
131
|
+
research subagent becomes useless but stays harmless (v3 audit #11: the
|
|
132
|
+
old fallback was an EMPTY middleware list, i.e. the subagent inherited
|
|
133
|
+
EVERY tool including write/execute — fail-open in the most dangerous
|
|
134
|
+
direction).
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def wrap_model_call(self, request, handler):
|
|
138
|
+
return handler(request.override(tools=[]))
|
|
139
|
+
|
|
140
|
+
async def awrap_model_call(self, request, handler):
|
|
141
|
+
return await handler(request.override(tools=[]))
|
|
116
142
|
|
|
117
143
|
|
|
118
144
|
def make_research_subagent_middleware():
|
|
@@ -126,8 +152,8 @@ def make_research_subagent_middleware():
|
|
|
126
152
|
|
|
127
153
|
Returns a list with a _ToolWhitelistMiddleware. If the deepagents
|
|
128
154
|
AgentMiddleware base class API is unavailable (very old or very new
|
|
129
|
-
version),
|
|
130
|
-
(
|
|
155
|
+
version), falls back to _DenyAllToolsMiddleware — the subagent sees no
|
|
156
|
+
tools rather than all of them (fail-closed; v3 audit #11).
|
|
131
157
|
"""
|
|
132
158
|
# Read-only tools the research subagent is allowed to use. Add new tools
|
|
133
159
|
# here ONLY after confirming they're safe for a read-only agent.
|
|
@@ -135,5 +161,8 @@ def make_research_subagent_middleware():
|
|
|
135
161
|
try:
|
|
136
162
|
return [_ToolWhitelistMiddleware(allowed=allowed)]
|
|
137
163
|
except Exception as e: # noqa: BLE001 — AgentMiddleware API may differ
|
|
138
|
-
_log.warning("Could not build _ToolWhitelistMiddleware: %s", e)
|
|
139
|
-
|
|
164
|
+
_log.warning("Could not build _ToolWhitelistMiddleware (deny-all fallback): %s", e)
|
|
165
|
+
try:
|
|
166
|
+
return [_DenyAllToolsMiddleware()]
|
|
167
|
+
except Exception: # noqa: BLE001 — even the fallback construction failed
|
|
168
|
+
return []
|
|
@@ -298,14 +298,38 @@ def _build_extra_tools(tools, skill_store, active_skills):
|
|
|
298
298
|
return extra
|
|
299
299
|
|
|
300
300
|
|
|
301
|
-
def _build_research_subagent():
|
|
301
|
+
def _build_research_subagent(gate=None, command_policy=None, hook_runner=None):
|
|
302
302
|
"""Return the research subagent spec (read-only, physically isolated).
|
|
303
303
|
|
|
304
304
|
Tool exclusion uses the compat layer (_deepagents_compat) so that a
|
|
305
305
|
deepagents API change degrades gracefully instead of crashing.
|
|
306
|
+
|
|
307
|
+
HooksMiddleware (2026-08-14 v3 audit #12): without it, ``task()``-delegated
|
|
308
|
+
work bypassed the user's PreToolUse/PostToolUse hooks entirely. The runner
|
|
309
|
+
is shared with the main agent (stateless across fire() calls).
|
|
310
|
+
|
|
311
|
+
Permission + CommandReview (v3 audit #11): execution-time enforcement on
|
|
312
|
+
top of the model-visibility whitelist — the whitelist filters what the
|
|
313
|
+
model SEES, these middlewares gate what actually RUNS. Belt and braces:
|
|
314
|
+
a whitelist bug or a future tool name collision can no longer turn the
|
|
315
|
+
"read-only" subagent into a writing one.
|
|
306
316
|
"""
|
|
307
317
|
from coderio.agent._deepagents_compat import make_research_subagent_middleware
|
|
308
318
|
|
|
319
|
+
middleware = make_research_subagent_middleware()
|
|
320
|
+
if hook_runner is not None and hook_runner.specs:
|
|
321
|
+
from coderio.agent.hooks import HooksMiddleware
|
|
322
|
+
|
|
323
|
+
middleware.insert(0, HooksMiddleware(hook_runner))
|
|
324
|
+
if gate is not None:
|
|
325
|
+
from coderio.agent.permission_middleware import PermissionMiddleware
|
|
326
|
+
|
|
327
|
+
middleware.append(PermissionMiddleware(gate))
|
|
328
|
+
from coderio.agent.command_review import CommandReviewMiddleware
|
|
329
|
+
from coderio.tools.command_policy import CommandPolicy
|
|
330
|
+
|
|
331
|
+
policy = command_policy or CommandPolicy.default()
|
|
332
|
+
middleware.append(CommandReviewMiddleware(policy, gate=gate))
|
|
309
333
|
return {
|
|
310
334
|
"name": "research",
|
|
311
335
|
"description": (
|
|
@@ -330,11 +354,11 @@ def _build_research_subagent():
|
|
|
330
354
|
"- The calling agent only sees your final message, not your "
|
|
331
355
|
"intermediate tool calls — make sure your answer is complete."
|
|
332
356
|
),
|
|
333
|
-
"middleware":
|
|
357
|
+
"middleware": middleware,
|
|
334
358
|
}
|
|
335
359
|
|
|
336
360
|
|
|
337
|
-
def _build_general_purpose_subagent(gate, command_policy, stream=None):
|
|
361
|
+
def _build_general_purpose_subagent(gate, command_policy, stream=None, hook_runner=None):
|
|
338
362
|
"""Return the general-purpose subagent spec with coderio's security middleware.
|
|
339
363
|
|
|
340
364
|
SECURITY FIX (2026-08-14 report P0-2): deepagents auto-injects a
|
|
@@ -359,13 +383,23 @@ def _build_general_purpose_subagent(gate, command_policy, stream=None):
|
|
|
359
383
|
is per-agent, not shared — the main gate can't see subagent tool calls
|
|
360
384
|
anyway), whose force-continue applies inside the subagent's loop. This
|
|
361
385
|
makes the subagent itself refuse to say "done" with unverified writes.
|
|
386
|
+
|
|
387
|
+
2026-08-14 v3 audit #12: also inject HooksMiddleware (outermost, same as
|
|
388
|
+
the main agent) — without it, task()-delegated work bypassed the user's
|
|
389
|
+
PreToolUse/PostToolUse hooks. The runner is shared (stateless fire()).
|
|
362
390
|
"""
|
|
363
391
|
from coderio.agent.command_review import CommandReviewMiddleware
|
|
364
392
|
from coderio.agent.harness_middleware import HarnessMiddleware
|
|
365
393
|
from coderio.tools.command_policy import CommandPolicy
|
|
366
394
|
|
|
367
395
|
middleware = []
|
|
368
|
-
#
|
|
396
|
+
# Hooks OUTERMOST (same order as the main agent): a hook deny happens
|
|
397
|
+
# before any other layer — including this subagent's permission prompts.
|
|
398
|
+
if hook_runner is not None and hook_runner.specs:
|
|
399
|
+
from coderio.agent.hooks import HooksMiddleware
|
|
400
|
+
|
|
401
|
+
middleware.append(HooksMiddleware(hook_runner))
|
|
402
|
+
# Harness next (same order as the main agent — see run_deep_agent): it
|
|
369
403
|
# observes every tool call the permission/command layers let through.
|
|
370
404
|
middleware.append(HarnessMiddleware(stream=stream))
|
|
371
405
|
if gate is not None:
|
|
@@ -553,8 +587,8 @@ def run_deep_agent(
|
|
|
553
587
|
"middleware": middleware,
|
|
554
588
|
"backend": backend,
|
|
555
589
|
"subagents": [
|
|
556
|
-
_build_research_subagent(),
|
|
557
|
-
_build_general_purpose_subagent(gate, command_policy, stream=stream),
|
|
590
|
+
_build_research_subagent(gate=gate, command_policy=command_policy, hook_runner=hook_runner),
|
|
591
|
+
_build_general_purpose_subagent(gate, command_policy, stream=stream, hook_runner=hook_runner),
|
|
558
592
|
],
|
|
559
593
|
}
|
|
560
594
|
if sp:
|
|
@@ -50,6 +50,11 @@ _TOOL_EVENTS = frozenset({"PreToolUse", "PostToolUse"})
|
|
|
50
50
|
|
|
51
51
|
DEFAULT_TIMEOUT = 60 # seconds; generous for linters, short enough not to brick a turn
|
|
52
52
|
|
|
53
|
+
# Per-EVENT total budget (v3 audit P1: serial hooks × 60s each would hang every
|
|
54
|
+
# tool call). Covers ALL matching hooks for one event fire; exhaustion skips
|
|
55
|
+
# the rest (fail-open). Overridable per HookRunner for tests.
|
|
56
|
+
DEFAULT_EVENT_BUDGET = 30.0
|
|
57
|
+
|
|
53
58
|
# stdout beyond this is dropped rather than injected (context budget guard).
|
|
54
59
|
_MAX_CONTEXT_CHARS = 10_000
|
|
55
60
|
|
|
@@ -108,12 +113,18 @@ class HookRunner:
|
|
|
108
113
|
_sessions_started: set[str] = set()
|
|
109
114
|
|
|
110
115
|
def __init__(
|
|
111
|
-
self,
|
|
116
|
+
self,
|
|
117
|
+
specs: list[HookSpec] | None,
|
|
118
|
+
project_dir: str,
|
|
119
|
+
session_id: str = "",
|
|
120
|
+
permission_mode: str = "",
|
|
121
|
+
event_budget: float = DEFAULT_EVENT_BUDGET,
|
|
112
122
|
) -> None:
|
|
113
123
|
self.specs = [s for s in (specs or []) if s.event in HOOK_EVENTS]
|
|
114
124
|
self.project_dir = project_dir
|
|
115
125
|
self.session_id = session_id
|
|
116
126
|
self.permission_mode = permission_mode
|
|
127
|
+
self.event_budget = event_budget
|
|
117
128
|
|
|
118
129
|
def has_event(self, event: str) -> bool:
|
|
119
130
|
return any(s.event == event for s in self.specs)
|
|
@@ -125,16 +136,41 @@ class HookRunner:
|
|
|
125
136
|
multi-hook semantics non-deterministic; determinism wins for a coding
|
|
126
137
|
agent. First blocker wins; later hooks still run (their side effects
|
|
127
138
|
are the point — e.g. logging — even when the event is already denied).
|
|
139
|
+
|
|
140
|
+
A per-EVENT budget (v3 audit P1: N hooks × 60s hanging on EVERY tool
|
|
141
|
+
call) caps the total time all matching hooks may take; when exhausted,
|
|
142
|
+
remaining hooks are skipped with an error note (fail-open — consistent
|
|
143
|
+
with the module's positioning: budget exhaustion never blocks).
|
|
128
144
|
"""
|
|
145
|
+
import time
|
|
146
|
+
|
|
129
147
|
tool_name = str(payload.get("tool_name", ""))
|
|
130
148
|
outcome = HookOutcome()
|
|
149
|
+
deadline = time.monotonic() + self.event_budget
|
|
131
150
|
try:
|
|
132
|
-
for spec in self.specs:
|
|
151
|
+
for i, spec in enumerate(self.specs):
|
|
133
152
|
if spec.event != event:
|
|
134
153
|
continue
|
|
135
154
|
if event in _TOOL_EVENTS and not spec.matches(tool_name):
|
|
136
155
|
continue
|
|
137
|
-
|
|
156
|
+
remaining = deadline - time.monotonic()
|
|
157
|
+
if remaining <= 0:
|
|
158
|
+
# Count the matching hooks NOT yet reached (self-audit
|
|
159
|
+
# 2026-08-18: the old slice self.specs[len(self.specs):]
|
|
160
|
+
# was always empty — the count never appeared). Filter by
|
|
161
|
+
# matcher too: a same-event hook whose matcher wouldn't
|
|
162
|
+
# have run anyway is not "skipped" (third-party audit
|
|
163
|
+
# caught the overcount).
|
|
164
|
+
pending = [
|
|
165
|
+
s
|
|
166
|
+
for s in self.specs[i:]
|
|
167
|
+
if s.event == event and (event not in _TOOL_EVENTS or s.matches(tool_name))
|
|
168
|
+
]
|
|
169
|
+
outcome.error += (
|
|
170
|
+
f"event budget ({self.event_budget}s) exhausted — {len(pending)} hook(s) skipped (fail-open); "
|
|
171
|
+
)
|
|
172
|
+
break
|
|
173
|
+
self._run_one(spec, event, payload, outcome, remaining)
|
|
138
174
|
except Exception as e: # noqa: BLE001 — a hook-layer bug must never break the turn
|
|
139
175
|
outcome.error += f"hook engine error (fail-open): {e}; "
|
|
140
176
|
# CONSUME the error (2026-08-14 v3 audit P1): a broken hook (missing
|
|
@@ -147,7 +183,14 @@ class HookRunner:
|
|
|
147
183
|
|
|
148
184
|
# ------------------------------------------------------------------ execution
|
|
149
185
|
|
|
150
|
-
def _run_one(
|
|
186
|
+
def _run_one(
|
|
187
|
+
self,
|
|
188
|
+
spec: HookSpec,
|
|
189
|
+
event: str,
|
|
190
|
+
payload: dict[str, Any],
|
|
191
|
+
outcome: HookOutcome,
|
|
192
|
+
remaining: float,
|
|
193
|
+
) -> None:
|
|
151
194
|
stdin_json = json.dumps(
|
|
152
195
|
{
|
|
153
196
|
"session_id": self.session_id,
|
|
@@ -159,13 +202,13 @@ class HookRunner:
|
|
|
159
202
|
default=str,
|
|
160
203
|
)
|
|
161
204
|
try:
|
|
162
|
-
exit_code, stdout, stderr, timed_out = self._spawn(spec, stdin_json)
|
|
205
|
+
exit_code, stdout, stderr, timed_out, effective_timeout = self._spawn(spec, stdin_json, remaining)
|
|
163
206
|
except Exception as e: # noqa: BLE001 — a broken hook must never crash the agent
|
|
164
207
|
outcome.error += f"hook {spec.command!r} failed to run: {e} (fail-open); "
|
|
165
208
|
return
|
|
166
209
|
|
|
167
210
|
if timed_out:
|
|
168
|
-
outcome.error += f"hook {spec.command!r} timed out after {
|
|
211
|
+
outcome.error += f"hook {spec.command!r} timed out after {effective_timeout}s (fail-open); "
|
|
169
212
|
return
|
|
170
213
|
|
|
171
214
|
# New names, not reusing stdout/stderr: mypy pins a variable's type to
|
|
@@ -196,12 +239,16 @@ class HookRunner:
|
|
|
196
239
|
else:
|
|
197
240
|
outcome.context += ("\n" if outcome.context else "") + text
|
|
198
241
|
|
|
199
|
-
def _spawn(self, spec: HookSpec, stdin_json: str) -> tuple[int, bytes, bytes, bool]:
|
|
200
|
-
"""Run the hook command with JSON on stdin.
|
|
242
|
+
def _spawn(self, spec: HookSpec, stdin_json: str, remaining: float) -> tuple[int, bytes, bytes, bool, int]:
|
|
243
|
+
"""Run the hook command with JSON on stdin.
|
|
244
|
+
|
|
245
|
+
Returns (exit_code, stdout, stderr, timed_out, effective_timeout).
|
|
246
|
+
effective_timeout = min(spec.timeout, event-budget remaining) — the
|
|
247
|
+
per-event budget tightens each hook so one slow hook can't starve the
|
|
248
|
+
rest of the turn (v3 audit P1).
|
|
201
249
|
|
|
202
250
|
On Windows prefers Git Bash (via detect_shell, same as the bash tool)
|
|
203
|
-
so POSIX-style commands behave; POSIX uses /bin/sh.
|
|
204
|
-
whole process tree (kill_process_tree) so piped children don't linger.
|
|
251
|
+
so POSIX-style commands behave; POSIX uses /bin/sh.
|
|
205
252
|
"""
|
|
206
253
|
import os
|
|
207
254
|
|
|
@@ -235,16 +282,29 @@ class HookRunner:
|
|
|
235
282
|
env=env,
|
|
236
283
|
start_new_session=(sys.platform != "win32"),
|
|
237
284
|
)
|
|
285
|
+
# ceil, not int(): remaining ≈ budget-ε on the first hook, and int()
|
|
286
|
+
# truncation would cut a 2s budget to 1s (caught by the CI OS matrix —
|
|
287
|
+
# deterministic on POSIX, timing-jitter-lucky on Windows).
|
|
288
|
+
import math
|
|
289
|
+
|
|
290
|
+
effective_timeout = max(1, min(spec.timeout, math.ceil(remaining)))
|
|
238
291
|
try:
|
|
239
|
-
stdout, stderr = proc.communicate(input=stdin_json.encode("utf-8"), timeout=
|
|
240
|
-
return proc.returncode, stdout or b"", stderr or b"", False
|
|
292
|
+
stdout, stderr = proc.communicate(input=stdin_json.encode("utf-8"), timeout=effective_timeout)
|
|
293
|
+
return proc.returncode, stdout or b"", stderr or b"", False, effective_timeout
|
|
241
294
|
except subprocess.TimeoutExpired:
|
|
242
295
|
kill_process_tree(proc)
|
|
296
|
+
# One-second grace, then abandon (v3 audit P1: the old 10s drain
|
|
297
|
+
# waited out pipe EOF that never comes when a pre-kill grandchild
|
|
298
|
+
# holds the write end on Windows — a timeout=2 hook took 12s). The
|
|
299
|
+
# output of a TIMED-OUT hook is never consumed anyway (hardcoded
|
|
300
|
+
# empty return), so there is nothing to wait for. Known limitation:
|
|
301
|
+
# kill_process_tree may miss Windows grandchildren forked before
|
|
302
|
+
# the job assignment; they leak but can no longer stall the turn.
|
|
243
303
|
try:
|
|
244
|
-
proc.communicate(timeout=
|
|
245
|
-
except Exception: # noqa: S110 — the kill already ran
|
|
304
|
+
proc.communicate(timeout=1)
|
|
305
|
+
except Exception: # noqa: S110 — grace expired; the kill already ran
|
|
246
306
|
pass
|
|
247
|
-
return 124, b"", b"", True
|
|
307
|
+
return 124, b"", b"", True, effective_timeout
|
|
248
308
|
|
|
249
309
|
|
|
250
310
|
class HooksMiddleware(AgentMiddleware):
|
|
@@ -183,19 +183,32 @@ def run_cmd(
|
|
|
183
183
|
provider: str = typer.Option(None, "--provider", help="Override provider_id."),
|
|
184
184
|
model: str = typer.Option(None, "--model", help="Override model name."),
|
|
185
185
|
permission: str = typer.Option(
|
|
186
|
-
"
|
|
186
|
+
"plan",
|
|
187
187
|
"--permission",
|
|
188
188
|
"-p",
|
|
189
|
-
help="Permission tier:
|
|
189
|
+
help="Permission tier: plan (default, read-only) or full (needs --dangerously-skip-permissions).",
|
|
190
190
|
),
|
|
191
191
|
session_id: str = typer.Option(None, "--session-id", help="Resume a prior session by id."),
|
|
192
192
|
quiet: bool = typer.Option(False, "--quiet", "-q", help="Print only the final result."),
|
|
193
|
+
skip_permissions: bool = typer.Option(
|
|
194
|
+
False,
|
|
195
|
+
"--dangerously-skip-permissions",
|
|
196
|
+
help="Run with full permissions (no prompts). Explicit opt-in — headless default is read-only plan mode.",
|
|
197
|
+
),
|
|
198
|
+
timeout: int = typer.Option(
|
|
199
|
+
0,
|
|
200
|
+
"--timeout",
|
|
201
|
+
help="Wall-clock limit in seconds (0 = unlimited). On timeout: exit 124. Recommended for CI.",
|
|
202
|
+
),
|
|
193
203
|
):
|
|
194
204
|
"""One-shot headless agent run (CI / scripting / benchmarks).
|
|
195
205
|
|
|
196
206
|
Streams tokens to stdout by default; tool progress goes to stderr. Never
|
|
197
207
|
prompts interactively — untrusted repo config or missing credentials are
|
|
198
|
-
hard errors.
|
|
208
|
+
hard errors. Default permission is plan (read-only); use
|
|
209
|
+
--dangerously-skip-permissions for full access. Exit codes: 0 success,
|
|
210
|
+
1 config error, 2 agent failure, 124 timeout. The command blacklist
|
|
211
|
+
still applies in every mode.
|
|
199
212
|
"""
|
|
200
213
|
from coderio.cli.run_cmd import run_headless
|
|
201
214
|
|
|
@@ -206,6 +219,8 @@ def run_cmd(
|
|
|
206
219
|
permission=permission,
|
|
207
220
|
session_id=session_id,
|
|
208
221
|
quiet=quiet,
|
|
222
|
+
skip_permissions=skip_permissions,
|
|
223
|
+
timeout=timeout,
|
|
209
224
|
)
|
|
210
225
|
|
|
211
226
|
|