agentic-cli 0.6.0__tar.gz → 0.6.1__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 (343) hide show
  1. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/.github/workflows/ci.yml +32 -0
  2. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/CHANGELOG.md +39 -0
  3. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/CLAUDE.md +5 -2
  4. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/PKG-INFO +6 -4
  5. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/README.md +4 -2
  6. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/pyproject.toml +1 -1
  7. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/__init__.py +1 -1
  8. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/app.py +64 -9
  9. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/message_processor.py +10 -2
  10. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/settings_command.py +34 -10
  11. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/config.py +5 -3
  12. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/file_utils.py +44 -10
  13. agentic_cli-0.6.1/src/agentic_cli/paths.py +35 -0
  14. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/settings_persistence.py +117 -23
  15. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/__init__.py +27 -6
  16. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/arxiv_source.py +32 -2
  17. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/arxiv_tools.py +8 -3
  18. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/document/compile.py +71 -30
  19. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/execution_tools.py +4 -2
  20. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/executor.py +14 -2
  21. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/factories.py +6 -5
  22. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/file_read.py +31 -10
  23. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/file_write.py +18 -12
  24. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/glob_tool.py +2 -2
  25. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/grep_tool.py +2 -1
  26. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/interaction_tools.py +11 -2
  27. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/tools.py +5 -1
  28. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/knowledge_tools.py +2 -2
  29. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/memory_tools.py +11 -4
  30. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/registry.py +16 -0
  31. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/__init__.py +9 -4
  32. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/jupyter_docker.py +6 -1
  33. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/jupyter_local.py +41 -2
  34. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/kernel_exec.py +29 -13
  35. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/manager.py +14 -0
  36. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/search.py +4 -1
  37. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/bubblewrap.py +1 -1
  38. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/policy.py +72 -16
  39. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/seatbelt.py +1 -1
  40. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/tool_resolver.py +30 -6
  41. agentic_cli-0.6.1/src/agentic_cli/tools/webfetch/_text.py +18 -0
  42. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/fetcher.py +14 -3
  43. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/robots.py +3 -1
  44. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/__init__.py +7 -1
  45. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/manager.py +4 -1
  46. agentic_cli-0.6.1/src/agentic_cli/workflow/adk/tool_error_plugin.py +59 -0
  47. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/base_manager.py +12 -6
  48. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/events.py +9 -0
  49. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/permission_wrap.py +8 -5
  50. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/__init__.py +2 -0
  51. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/capabilities.py +27 -2
  52. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/engine.py +96 -22
  53. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/matchers.py +56 -8
  54. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/prompt.py +22 -11
  55. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/store.py +17 -2
  56. agentic_cli-0.6.1/tests/cli/test_settings_apply_rollback.py +178 -0
  57. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_permission_langgraph.py +95 -0
  58. agentic_cli-0.6.1/tests/permissions/test_capability_targets.py +236 -0
  59. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_engine.py +28 -26
  60. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_matchers.py +91 -11
  61. agentic_cli-0.6.1/tests/permissions/test_no_approver.py +148 -0
  62. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_prompt.py +7 -6
  63. agentic_cli-0.6.1/tests/permissions/test_target_resolution.py +205 -0
  64. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_arxiv_download_security.py +5 -3
  65. agentic_cli-0.6.1/tests/test_google_search_deprecation.py +155 -0
  66. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_memory.py +21 -6
  67. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_message_processor_render.py +67 -0
  68. agentic_cli-0.6.1/tests/test_paths.py +68 -0
  69. agentic_cli-0.6.1/tests/test_settings_save_changes.py +165 -0
  70. agentic_cli-0.6.1/tests/tools/test_capability_declarations.py +134 -0
  71. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_document_compile.py +160 -45
  72. agentic_cli-0.6.1/tests/tools/test_document_compile_latex.py +140 -0
  73. agentic_cli-0.6.1/tests/tools/test_file_mode.py +84 -0
  74. agentic_cli-0.6.1/tests/tools/test_local_kernel_timeout.py +139 -0
  75. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_os_sandbox.py +9 -6
  76. agentic_cli-0.6.1/tests/tools/test_os_sandbox_protected.py +190 -0
  77. agentic_cli-0.6.1/tests/tools/test_path_contract.py +211 -0
  78. agentic_cli-0.6.1/tests/tools/test_python_exec_timeouts.py +119 -0
  79. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_docker_backend.py +2 -2
  80. agentic_cli-0.6.1/tests/tools/test_sandbox_session_ids.py +56 -0
  81. agentic_cli-0.6.1/tests/tools/test_tool_declarations.py +55 -0
  82. agentic_cli-0.6.1/tests/tools/test_tools_return_errors.py +214 -0
  83. agentic_cli-0.6.1/tests/tools/test_webfetch_status_charset.py +135 -0
  84. agentic_cli-0.6.0/tests/tools/test_document_compile_latex.py +0 -37
  85. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/.gitignore +0 -0
  86. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/LICENSE +0 -0
  87. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/environment.yml +0 -0
  88. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/arxiv_demo.py +0 -0
  89. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/fileops_demo.py +0 -0
  90. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/hello_agent.py +0 -0
  91. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/hello_langgraph.py +0 -0
  92. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/jobs_demo.py +0 -0
  93. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/memory_demo.py +0 -0
  94. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/__init__.py +0 -0
  95. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/__main__.py +0 -0
  96. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/agents.py +0 -0
  97. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/app.py +0 -0
  98. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/commands.py +0 -0
  99. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/data/benchmarks.csv +0 -0
  100. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/settings.py +0 -0
  101. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/skills/report-writer/SKILL.md +0 -0
  102. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/skills/report-writer/assets/report_template.tex +0 -0
  103. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/shell_demo.py +0 -0
  104. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/webfetch_demo.py +0 -0
  105. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/websearch_demo.py +0 -0
  106. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/requirements-dev.txt +0 -0
  107. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/requirements.txt +0 -0
  108. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/__init__.py +0 -0
  109. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/builtin_commands.py +0 -0
  110. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/commands.py +0 -0
  111. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/job_monitor.py +0 -0
  112. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/sandbox_render.py +0 -0
  113. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/settings_introspection.py +0 -0
  114. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/usage_tracker.py +0 -0
  115. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/workflow_controller.py +0 -0
  116. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/constants.py +0 -0
  117. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/__init__.py +0 -0
  118. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/_bm25_backends.py +0 -0
  119. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/_mock_bm25.py +0 -0
  120. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/_mocks.py +0 -0
  121. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/bm25_index.py +0 -0
  122. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/concepts.py +0 -0
  123. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/embeddings.py +0 -0
  124. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/manager.py +0 -0
  125. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/models.py +0 -0
  126. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/sidecar.py +0 -0
  127. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/sources.py +0 -0
  128. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/vector_store.py +0 -0
  129. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/logging.py +0 -0
  130. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/persistence/__init__.py +0 -0
  131. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/settings_mixins.py +0 -0
  132. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/__init__.py +0 -0
  133. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/planning.py +0 -0
  134. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/state_tools.py +0 -0
  135. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/tasks.py +0 -0
  136. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/adk/__init__.py +0 -0
  137. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/adk/state_tools.py +0 -0
  138. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/document/__init__.py +0 -0
  139. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/__init__.py +0 -0
  140. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/backends.py +0 -0
  141. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/manager.py +0 -0
  142. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/langgraph/__init__.py +0 -0
  143. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/langgraph/state_tools.py +0 -0
  144. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/pdf_utils.py +0 -0
  145. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/__init__.py +0 -0
  146. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/base.py +0 -0
  147. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/container_runtime.py +0 -0
  148. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/detect.py +0 -0
  149. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/driver.py +0 -0
  150. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/models.py +0 -0
  151. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/__init__.py +0 -0
  152. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/audit.py +0 -0
  153. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/classifier.py +0 -0
  154. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/config.py +0 -0
  155. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/executor.py +0 -0
  156. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/models.py +0 -0
  157. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/__init__.py +0 -0
  158. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/base.py +0 -0
  159. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/detect.py +0 -0
  160. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/seatbelt_profile.py +0 -0
  161. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/path_analyzer.py +0 -0
  162. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/preprocessor.py +0 -0
  163. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/risk_assessor.py +0 -0
  164. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/sandbox.py +0 -0
  165. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/tokenizer.py +0 -0
  166. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/__init__.py +0 -0
  167. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/permissions.py +0 -0
  168. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/store.py +0 -0
  169. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/toolset.py +0 -0
  170. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/__init__.py +0 -0
  171. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/converter.py +0 -0
  172. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/summarizer.py +0 -0
  173. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/transport.py +0 -0
  174. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/validator.py +0 -0
  175. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch_tool.py +0 -0
  176. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/__init__.py +0 -0
  177. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/anthropic_llm.py +0 -0
  178. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/event_processor.py +0 -0
  179. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/permission_plugin.py +0 -0
  180. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/plugins.py +0 -0
  181. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/task_progress_plugin.py +0 -0
  182. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/transfer_tool_description.py +0 -0
  183. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk_config_bridge.py +0 -0
  184. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/agent_loader.py +0 -0
  185. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/config.py +0 -0
  186. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/factory.py +0 -0
  187. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/__init__.py +0 -0
  188. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/graph_builder.py +0 -0
  189. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/manager.py +0 -0
  190. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/persistence/__init__.py +0 -0
  191. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +0 -0
  192. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/persistence/stores.py +0 -0
  193. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/state.py +0 -0
  194. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/mcp.py +0 -0
  195. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/model_settings.py +0 -0
  196. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/models.py +0 -0
  197. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/rules.py +0 -0
  198. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/retry.py +0 -0
  199. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/service_registry.py +0 -0
  200. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/sessions.py +0 -0
  201. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/settings.py +0 -0
  202. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/tool_summaries.py +0 -0
  203. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/__init__.py +0 -0
  204. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/__init__.py +0 -0
  205. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_job_monitor.py +0 -0
  206. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_process_resume.py +0 -0
  207. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_resume_coordinator.py +0 -0
  208. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_sandbox_render.py +0 -0
  209. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_sessions_command.py +0 -0
  210. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_settings_ui_scope.py +0 -0
  211. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_turn_boundary.py +0 -0
  212. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_turn_retry_safety.py +0 -0
  213. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/conftest.py +0 -0
  214. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/demo_isolation.py +0 -0
  215. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/event_replay.py +0 -0
  216. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/__init__.py +0 -0
  217. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/console_smoke.py +0 -0
  218. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_report_writer_skill.py +0 -0
  219. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_agents.py +0 -0
  220. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_app.py +0 -0
  221. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_pty.py +0 -0
  222. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_wheel.py +0 -0
  223. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/__init__.py +0 -0
  224. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/conftest.py +0 -0
  225. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/helpers.py +0 -0
  226. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_adk_claude_live.py +0 -0
  227. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_adk_integration.py +0 -0
  228. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_kb_research_flow.py +0 -0
  229. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_langgraph_integration.py +0 -0
  230. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_live.py +0 -0
  231. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_live_durable_sessions.py +0 -0
  232. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_live_job_resume.py +0 -0
  233. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_permission_adk.py +0 -0
  234. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_research_scenarios.py +0 -0
  235. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/__init__.py +0 -0
  236. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_capabilities.py +0 -0
  237. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_grant_provenance.py +0 -0
  238. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_rules.py +0 -0
  239. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_store.py +0 -0
  240. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_trust_model.py +0 -0
  241. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_adk_event_processor.py +0 -0
  242. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_adk_plugins.py +0 -0
  243. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_arxiv_tools.py +0 -0
  244. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_bm25_index.py +0 -0
  245. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_commands.py +0 -0
  246. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_concept_store.py +0 -0
  247. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_concurrent_stores.py +0 -0
  248. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_config.py +0 -0
  249. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_config_trust.py +0 -0
  250. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_context_trimming.py +0 -0
  251. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_context_window.py +0 -0
  252. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_dual_thinking_boxes.py +0 -0
  253. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_embeddings.py +0 -0
  254. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_executor_security.py +0 -0
  255. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_file_utils_atomic.py +0 -0
  256. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_file_utils_nofollow.py +0 -0
  257. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_grep_tool_cache.py +0 -0
  258. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_grep_tool_security.py +0 -0
  259. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_import_surface.py +0 -0
  260. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_input_callback.py +0 -0
  261. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_concepts.py +0 -0
  262. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_helpers.py +0 -0
  263. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_index_md.py +0 -0
  264. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_ingest_log.py +0 -0
  265. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_sidecar.py +0 -0
  266. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_tool_renames.py +0 -0
  267. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_knowledge_base.py +0 -0
  268. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_knowledge_tools.py +0 -0
  269. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_langgraph.py +0 -0
  270. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_memory_thread_safety.py +0 -0
  271. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_message_processor_cancel.py +0 -0
  272. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_model_registry.py +0 -0
  273. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_packaging.py +0 -0
  274. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_planning.py +0 -0
  275. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_retry.py +0 -0
  276. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_settings_persistence.py +0 -0
  277. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_task_tools.py +0 -0
  278. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_token_caching.py +0 -0
  279. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_tool_summaries.py +0 -0
  280. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_tools.py +0 -0
  281. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_update_setting.py +0 -0
  282. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_usage_tracker.py +0 -0
  283. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_vector_store.py +0 -0
  284. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_webfetch.py +0 -0
  285. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_workflow.py +0 -0
  286. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_workflow_controller.py +0 -0
  287. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/__init__.py +0 -0
  288. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_adk_state_tools.py +0 -0
  289. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_bubblewrap.py +0 -0
  290. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_core_planning.py +0 -0
  291. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_core_tasks.py +0 -0
  292. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_executor_security.py +0 -0
  293. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_factories.py +0 -0
  294. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_glob_grep_containment.py +0 -0
  295. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_jobs.py +0 -0
  296. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_langgraph_state_tools.py +0 -0
  297. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_registry.py +0 -0
  298. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_registry_identity.py +0 -0
  299. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox.py +0 -0
  300. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_container_runtime.py +0 -0
  301. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_container_session.py +0 -0
  302. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_detect.py +0 -0
  303. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_docker_integration.py +0 -0
  304. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_docker_live.py +0 -0
  305. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_driver.py +0 -0
  306. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_kernel_exec.py +0 -0
  307. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_settings.py +0 -0
  308. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_transfer.py +0 -0
  309. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_seatbelt_profile.py +0 -0
  310. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_shell_security.py +0 -0
  311. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_state_tool_aliases.py +0 -0
  312. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_webfetch_ssrf.py +0 -0
  313. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/__init__.py +0 -0
  314. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_active_turn_context.py +0 -0
  315. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_agent_construction.py +0 -0
  316. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_claude.py +0 -0
  317. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_claude_adaptive.py +0 -0
  318. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_config_bridge.py +0 -0
  319. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_direct_anthropic.py +0 -0
  320. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_job_resume.py +0 -0
  321. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_mcp_permissions.py +0 -0
  322. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_transfer_tool.py +0 -0
  323. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_agent_graph.py +0 -0
  324. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_agent_loader.py +0 -0
  325. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_backend_isolation.py +0 -0
  326. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_base_manager_init_lock.py +0 -0
  327. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_base_manager_permissions.py +0 -0
  328. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_langgraph_ignored_fields.py +0 -0
  329. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_lifecycle_races.py +0 -0
  330. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_mcp_config.py +0 -0
  331. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_memory_wiring.py +0 -0
  332. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_model_settings.py +0 -0
  333. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_model_validation.py +0 -0
  334. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_permission_tool_identity.py +0 -0
  335. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_resource_ownership.py +0 -0
  336. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_session_store.py +0 -0
  337. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_settings_permissions.py +0 -0
  338. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_skills.py +0 -0
  339. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_task_progress_plugin.py +0 -0
  340. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_thinking_config.py +0 -0
  341. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_tool_assembly.py +0 -0
  342. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_tool_resolver.py +0 -0
  343. {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_turn_serialization.py +0 -0
@@ -68,6 +68,38 @@ jobs:
68
68
  conda run -n agenticcli python -m pytest
69
69
  tests/examples/test_research_demo_wheel.py -m wheel -v
70
70
 
71
+ latex-engine:
72
+ name: LaTeX engine tests
73
+ runs-on: ubuntu-latest
74
+ defaults:
75
+ run:
76
+ shell: bash -el {0}
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+
80
+ # compile_document's lockdown (no shell escape, paranoid file access) is
81
+ # enforced by TeX itself, so it can only be verified against a real engine.
82
+ - name: Install TeX Live (pdflatex + latexmk)
83
+ run: |
84
+ sudo apt-get update
85
+ sudo apt-get install -y --no-install-recommends \
86
+ texlive-latex-base texlive-latex-recommended latexmk
87
+
88
+ - name: Set up conda env (agenticcli)
89
+ uses: conda-incubator/setup-miniconda@v3
90
+ with:
91
+ miniforge-version: latest
92
+ environment-file: environment.yml
93
+ activate-environment: agenticcli
94
+
95
+ # Targets the file: a bare `-m latex` still collects the whole suite.
96
+ - name: Run LaTeX engine tests (fail if the engine is missing)
97
+ env:
98
+ LATEX_REQUIRE: "1"
99
+ run: >
100
+ conda run -n agenticcli python -m pytest
101
+ tests/tools/test_document_compile_latex.py -m latex -v
102
+
71
103
  docker-isolation:
72
104
  name: Docker isolation tests
73
105
  runs-on: ubuntu-latest
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.1] - 2026-09-27
11
+
12
+ Security and stability fixes for 0.6.0. The permission engine now judges the
13
+ location a tool actually acts on, approving a search tool no longer approves
14
+ every URL, the OS sandbox protects credentials, the app's config and `.git`,
15
+ and `compile_document` can no longer read files outside the project or run
16
+ other programs. Tool errors, unanswerable permission prompts and HTTP error
17
+ pages no longer end a turn or pose as content, and the Python timeouts take
18
+ effect. Two interface changes, see *Changed*: `web_search` declares
19
+ `search.web`, and `compile_document` requires `output_pdf`.
20
+
21
+ ### Security
22
+ - **The permission engine now judges the location a tool actually acts on.** `${workdir}`, `${home}` and `${app_name}` were expanded in tool arguments as well as in rules, so a crafted path could be judged inside the project while the tool acted outside it; and the engine expanded `~` and anchored relative paths to the project while the file tools did neither. Both sides now resolve arguments with one function, `agentic_cli.paths.resolve_path`: placeholders expand only in rule patterns, `~` expands in the tools too, relative paths resolve against the process's current directory, saved grants load literally, an argument of `*` is a literal name rather than the wildcard (so approving it can no longer grant every read), and an argument that cannot name a path is denied. `compile_document` also refuses an `output_pdf` that is a symlink and fixes its destination before compiling.
23
+ - **Approving a search tool no longer approves every URL.** `web_search` and the arXiv tools declared `http.read` with no target, so an approval was saved as `http.read *` and silently covered `web_fetch` and `kb_ingest_url` for any URL. The arXiv tools now declare their fixed arXiv endpoints with the new `Capability(..., target=...)`, and the PDF download refuses anything outside `https://arxiv.org/pdf/`. Filesystem, network and shell capabilities with no target warn at registration (an error in 0.7.0) and are approved one call at a time, and `http.read *`-style grants saved by 0.6.0 are dropped on load. A filesystem grant now covers the directory it names instead of that directory's parent, and never the filesystem root or a directory above home.
24
+ - **LangGraph no longer grants a tool's permissions by name.** Its permission wrapper looked tools up by name, so on the LangGraph backend a plain function sharing a registered tool's name inherited that tool's capabilities, or ran unchecked if the tool was `EXEMPT`. It now resolves tools by registry identity, as the ADK backend has since 0.6.0.
25
+ - **The OS sandbox around `execute_python` protects credentials, the app's config and `.git`.** Sandboxed code could read `~/.ssh`, `~/.aws` and other credential stores, the app's own `~/.{app_name}` (settings, permission grants, `.env`) and the project's `.env`; only `.git/hooks` and `.git/config` were write-protected, so the whole `.git` could be moved aside and replaced; and when the app was started from the home directory, all of home was writable. These locations are now denied whatever the configured policy says, the whole `.git` is write-protected, and a working directory that is home, an ancestor of home, or `/` is no longer made writable.
26
+ - **Sandbox sessions can no longer reach each other.** `sandbox_execute` session IDs were sanitized into directory and container names, so an empty ID named the directory holding every session (which the Docker backend mounts into the container) and IDs such as `a.b` and `a_b` shared one. IDs must now be 1–64 letters, digits, `-` or `_`, and anything else is refused. Docker container names also carry a per-process suffix, so two app processes using the same session ID no longer collide.
27
+ - **`compile_document` can no longer read files outside the project or run other programs.** TeX reads any file the user can read by default, so a document could `\input` an absolute path, a `..` path or a project dotfile such as `.env` (or dump any file with `\pdffiledump`), and the contents came back to the model in the log or the PDF — past the `filesystem.read` rules. And while `pdflatex` ran with `-no-shell-escape`, the `pdflatex` runs `latexmk` makes got TeX Live's restricted shell escape, which runs a list of programs. The TeX process now runs with shell escape off and paranoid file access (`shell_escape=f`, `openin_any=p`, `openout_any=p`), and the host's `TEXMFOUTPUT` is no longer passed through. Documents still find files by relative name in their own directory, `assets_dir` and the TeX installation. A new CI job verifies this against a real TeX Live.
28
+ - **`/settings` saves only what you changed, and never a credential.** It saved the whole live settings object, so a value that came from the environment for one run (`os_sandbox_enabled=false`, `raw_llm_logging=true`) or was derived at startup was written to the config and kept applying, and an application's own API-key fields were written in plaintext. It now saves exactly the fields the dialog changed (`save_settings(keys=...)`), nothing when nothing changed, and no field named like a credential or typed `SecretStr` is ever written.
29
+
30
+ ### Changed
31
+ - **`web_search` declares its own capability, `search.web`.** Sending queries to the configured search provider is a different decision from fetching arbitrary URLs. Rules written for `http.read` no longer apply to `web_search`; add a `search.web` rule if you had one.
32
+ - **`compile_document` requires `output_pdf`.** Without it, the PDF was written next to the source with no `filesystem.write` check, so it could overwrite an existing PDF there that nothing had approved. The destination is now always given and always checked; an empty value is refused. The report-writer skill and the research demo already pass it.
33
+
34
+ ### Deprecated
35
+ - **`agentic_cli.tools.google_search_tool` — deprecated, removed in 0.7.0.** It re-exported ADK's built-in `google_search`, which runs inside the model, so the permission engine never checks it. Use `agentic_cli.tools.web_search`. This reverses the 0.1.2 advice to use ADK's tool; the web search client removed then returned in 0.3.2. For native Google Search, import `GoogleSearchTool` from `google.adk.tools.google_search_tool`; in this framework it works only as an agent's sole tool, with `include_state_tools=False`. The name is no longer in `agentic_cli.tools.__all__`, importing the package loads no ADK code, every use warns, and a reference in agent configuration warns with a visible `FutureWarning`.
36
+
37
+ ### Fixed
38
+ - **A tool result containing `[/…]` no longer fails the turn in the CLI.** Tool names and output were inserted into Rich markup unescaped, so an error such as `File not found: notes[/draft].md` raised a markup error after the tool had already run, and text like `[bold red]` restyled the output. They are now escaped and shown literally.
39
+ - **`update_memory` no longer breaks every Gemini request.** Its `tags` default was an internal `object()` marker, which ADK copied into the tool declaration, so any ADK agent on Gemini that included the tool failed before reaching the model. `tags` now defaults to `None`, which leaves tags unchanged, and an empty list clears them: models often send `null` for an argument they mean to omit, so `null` no longer wipes tags. A test now serializes every framework tool's declaration.
40
+ - **A permission prompt nobody can answer now denies the call instead of ending the turn.** With no input callback installed (a headless run, or a script driving the manager directly), a tool call with no matching rule raised out of the permission check and aborted the turn. The engine now denies the call with a reason the model can read, and a prompt that fails for any other reason is also a denial; cancelling the turn still cancels it. `ask_clarification` returns an error result in the same situation. `request_user_input` now raises `UserInputUnavailable`, a `RuntimeError` subclass exported from `agentic_cli.workflow`.
41
+ - **A tool that raises no longer ends the turn on the ADK backend.** ADK re-raises a tool's exception unless a plugin answers for it, so one bad call aborted the whole turn. The new `ToolErrorPlugin` turns the exception into `{"success": False, "error": ...}` so the model can recover; a call to a tool that does not exist gets the same treatment. LangGraph already did this. The framework tools that raised on ordinary bad input now return an error themselves: `write_file` when a parent path is a file, `edit_file` with an invalid regex replacement such as `\d`, `diff_compare` on a binary or unreadable file, and `job_list` with an unknown state.
42
+ - **`web_fetch` no longer treats an error page as the page.** A 404 or 500 response was returned as fetched content: `web_fetch` summarized it, `kb_ingest_url` stored it in the knowledge base, an arXiv PDF download accepted it, and it stayed cached for the cache lifetime. A 4xx or 5xx response is now a failure that names the status and is not cached. A `charset` the server declares that Python cannot decode with no longer raises: the body is decoded as UTF-8, and robots.txt rules still apply in that case instead of being ignored.
43
+ - **A `/settings` change that fails is undone instead of saved.** The dialog applied fields one at a time: when a later value was invalid, or the workflow could not restart with the new model or orchestrator, it reported the error but kept the earlier changes, and `/settings` saved them, so a model the app could not start with was applied again on the next run. After a failed model switch the session also stayed down on that model. `apply_settings` is now all or nothing: on failure every field is restored, the workflow is restarted on the model it was running, nothing is saved, and it returns `False` (`True` on success).
44
+ - **`python_executor_timeout` and `sandbox_timeout` now take effect.** `execute_python` and `sandbox_execute` had their own defaults in the tool signature (30 s and 120 s) and always passed them on, so the configured timeouts were never used. A call that gives no timeout now uses the setting, a timeout the call does give is used as given, and zero or a negative value is refused.
45
+ - **A timed-out `sandbox_execute` cell on the local backend is stopped, and the session keeps working.** The local Jupyter backend applied its timeout to the gap between two outputs, so a cell that printed steadily never timed out; and when a timeout did fire, the cell kept running, so every later call in that session queued behind it and timed out as well. The timeout now bounds the whole cell. When it expires, the cell is interrupted and the session's variables are kept; a cell that ignores the interrupt gets a new kernel, and the result says the session's variables were lost. The Docker backend already worked this way.
46
+ - **`write_file` and `edit_file` no longer change a file's permissions.** Atomic writes go through a temp file that is always created 0600 and then replaces the original, so editing a `0755` script made it non-executable and unreadable to anyone else, and every new file was 0600. An existing file now keeps its permission bits and a new file gets the process default (0666 minus the umask), as a plain write would; setuid/setgid are not carried over. The framework's own state files (settings, grants, memory, knowledge base) stay private.
47
+ - **`compile_document` allows 5 minutes by default instead of 2.** The timeout covers the whole build (several `pdflatex` passes plus `bibtex`/`biber`), which a long or figure-heavy document can take more than two minutes to finish. An explicit `timeout_s` is still used as given.
48
+
10
49
  ## [0.6.0] - 2026-08-04
11
50
 
12
51
  Long-running jobs, durable sessions, a Docker sandbox backend, five P0 security
@@ -23,6 +23,7 @@ agentic-cli/
23
23
  │ ├── settings_persistence.py # Trust-split save (PROJECT_SETTABLE_KEYS → project, rest → user config; excludes SECRET_FIELDS)
24
24
  │ ├── constants.py # Shared constants, truncate()
25
25
  │ ├── file_utils.py # atomic_write_json / atomic_write_text
26
+ │ ├── paths.py # resolve_path: the one interpretation of a path argument
26
27
  │ ├── logging.py
27
28
  │ ├── cli/
28
29
  │ │ ├── app.py # BaseCLIApp
@@ -53,6 +54,7 @@ agentic-cli/
53
54
  │ │ │ ├── event_processor.py # ADKEventProcessor
54
55
  │ │ │ ├── permission_plugin.py # PermissionPlugin (gates tool calls)
55
56
  │ │ │ ├── task_progress_plugin.py # Emits TASK_PROGRESS events
57
+ │ │ │ ├── tool_error_plugin.py # Tool exception → error result (turn survives)
56
58
  │ │ │ └── plugins.py # LLM traffic logging (raw_llm_logging)
57
59
  │ │ └── langgraph/ # LangGraph orchestrator
58
60
  │ │ ├── manager.py # LangGraphWorkflowManager
@@ -155,10 +157,11 @@ Workflow:
155
157
  - **UI-agnostic workflow**: WorkflowEvent objects can be consumed by any UI
156
158
 
157
159
  ### Key Design Patterns
158
- - **Tool error handling**: All tools return `{"success": bool, ...}` dicts. Never raise `ToolError`.
160
+ - **Tool error handling**: All tools return `{"success": bool, ...}` dicts. Never raise `ToolError`. An exception that still escapes a tool is a bug, but it costs one call rather than the turn: on ADK, `workflow/adk/tool_error_plugin.py::ToolErrorPlugin` turns it (and a call to a tool name that does not exist) into an error dict; LangGraph's `ToolNode` runs with `handle_tool_errors=True`.
159
161
  - **Tool registration**: Use `@register_tool(category=..., capabilities=..., description=...)` decorator. `capabilities=` is required — pass `EXEMPT` for tools that need no permission check or a list of `Capability(name, target_arg=...)` tuples the engine matches against rules. Tools are auto-discovered via the global `ToolRegistry`.
160
162
  - **ADK transfer-tool description**: ADK builds `transfer_to_agent`'s model-visible description from its docstring, which through 1.37.0 tells the model to "use TransferToAgentTool instead of this function directly" — advice meant for Python callers that made Gemini emit the class name, which ADK then rejects. `workflow/adk/transfer_tool_description.py` is a `before_model_callback` plugin that rewrites that description on the prepared `LlmRequest`: only when the tool is the **exact** native `TransferToAgentTool` (an application tool sharing the name is untouched) and only while the misleading text is present, so it is idempotent and becomes a **no-op** once the installed ADK ships a corrected docstring (upstream fixed it in 2.x). Only `description` is written — declaration name, parameter schema, required fields and the agent-name enum are preserved, and the upstream function's `__doc__` is never mutated.
161
163
  - **Permissions**: `workflow/permissions/` holds a framework-independent engine that evaluates declared capabilities against rules from four sources (builtin, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). Interactive "Allow always" grants persist to `~/.{app_name}/project_grants.json` keyed by resolved project path — user-owned, never into the project settings file a repo could ship. ADK + LangGraph gate tool calls via `workflow/adk/permission_plugin.py::PermissionPlugin` and `workflow/langgraph/permission_wrap.py::wrap_tool_for_permission`.
164
+ - **Permission targets vs. patterns**: a tool argument is a *target*, a rule's target is a *pattern*. Matchers canonicalize them with different methods: `canonicalize_pattern` expands `${workdir}`/`${home}`/`${app_name}` and anchors relative filesystem patterns to the project; `canonicalize_target` expands nothing and resolves a path exactly as the tool will. Any tool that takes a path argument must resolve it with `agentic_cli.paths.resolve_path` (the same function `PathMatcher.canonicalize_target` uses), so the engine judges the location the tool acts on. `tests/tools/test_path_contract.py` fails when a new filesystem tool has no contract case. Filesystem, `http.*` and shell capabilities must name their resource (`target_arg=` or a fixed `target=`): a targetless one resolves to the wildcard, so it warns at registration and is never remembered as a grant, and saved `http.read *`-style grants are dropped on load. Filesystem grants cover the named directory (a file's own directory), never `/` or anything above home.
162
165
  - **Service registry**: Tools access services and shared state via `get_service(key)` from `workflow.service_registry`. A single ContextVar holds a `dict[str, Any]` set by the workflow manager during processing. Complex services (KBManager, SandboxManager, MemoryStore) are lazily created; simple state (plan string, task list) lives directly in the registry dict.
163
166
  - **Manager detection**: tools declare their own service needs — `@register_tool(..., requires="kb_manager")` (or a tuple for several) — and the key is validated against `service_registry.KNOWN_SERVICE_KEYS` at registration (only *constructible* services are declarable; `user_kb_manager` is created together with `kb_manager`). `BaseWorkflowManager._detect_required_managers()` reads that metadata off the registry for each agent's tools (by registry identity, so `register(func, name=...)`'s original callable still declares its services); `_build_services()` then lazily constructs only the services actually needed, into a local dict that is released in full if a later constructor raises (nothing is published, so nothing else could close it). A downstream tool may request any framework-provided service without editing the framework; there is no mechanism for registering new service *types*, and no central name→service map.
164
167
  - **Canonical tool names + permission identity**: `ToolDefinition.name` is the single identity. `register_tool(name="public_name")` wraps the callable so `func.__name__` is the registered name (backends derive the model-visible name from the callable). Permission gating **and tool assembly** resolve through a **registry-owned identity binding**: each `ToolRegistry` owns a `id(obj) → (weakref, definition)` map (`registry.bind_identity()`/`identify()`; the module-level `tools.registry.bind_tool_identity()`/`identify_tool()` answer for `get_registry()`). Every hit is confirmed with `is` against the weak reference, so neither a name, a class name, nor a forged `__eq__`/`__hash__` can stand in for it, and a recycled address inherits nothing. Identity is **per registry** — a tool registered into an application's own `ToolRegistry` is not one of the framework's, so it stays untouched during assembly and is denied at permission time — which is also what lets a short-lived registry, its definitions and their closures be garbage collected. Everything the framework issues is bound at its construction site: registered callables, factory service-bound variants, renamed wrappers, and the native ADK tool objects the framework builds (skill tools, in `tools/skills/toolset.py`). `workflow/adk/permission_plugin.py` unwraps `.func` only for the exact ADK types whose contract is to call it (`FunctionTool`, `LongRunningFunctionTool`, and ADK's own `TransferToAgentTool`), and gates genuine `McpTool` instances (`isinstance`, since ADK creates them on connect) under a synthetic `mcp` capability. `TransferToAgentTool` is a `FunctionTool` **subclass** ADK auto-injects into any agent with `sub_agents`; matching only exact `FunctionTool` denied the built-in routing tool as unregistered, so delegation could not work at all. It is trusted on exactly the same terms as the others — ADK constructs it as `super().__init__(func=transfer_to_agent)` and overrides only `_get_declaration()` (to add the agent-name enum), never `run_async`, so what it invokes is still exactly `self.func`. Listing the **exact class** keeps every other subclass, forged object and same-named tool denied; no name authority is restored. Anything unbound is denied — and, in assembly (service detection, service-tool substitution, canonicalization, long-running wrapping), left exactly as the application supplied it: a plain callable named `kb_search` is not the framework's tool and must not be given its services, its service-bound variant, or its long-running contract. Substitution additionally requires the service variant to *be* that same definition (`identify_tool(variant) is definition`); factories bind each closure to the exact module-level tool it re-binds, so a tool an application has taken over keeps its own implementation.
@@ -169,7 +172,7 @@ Workflow:
169
172
  - **Session identity**: durable sessions are addressed by `SessionRef(app_name, user_id, session_id)` (`workflow/sessions.py`). Every session hook (`session_exists`/`list_sessions`/`delete_session`/`recent_messages`/`load_session`) takes an optional `user_id`, defaulting to `settings.default_user` only when the caller omits it. Backends without a session store leave `supports_sessions` False, and the base hooks raise `NotImplementedError` rather than answering with a misleading `False`/`[]`.
170
173
  - **Active turn**: the in-flight `(user, session)` is a `ContextVar` (`workflow/sessions.py::get_active_turn`), set with a token by `_workflow_context()` — concurrent turns on one manager stay isolated and nesting restores the outer turn.
171
174
  - **Resource ownership**: `cleanup()` is idempotent and awaits an async `close()` on owned resources (`BaseWorkflowManager._aclose_owned`); `WorkflowController.close()` is the single shutdown path (cancel init → shut executor → clean manager).
172
- - **Atomic writes**: Use `atomic_write_json`/`atomic_write_text` from `file_utils.py` for file persistence.
175
+ - **Atomic writes**: Use `atomic_write_json`/`atomic_write_text` from `file_utils.py` for file persistence. They create files private (0600), right for the framework's own state; a write to a file the user owns (`write_file`, `edit_file`) passes `preserve_mode=True`, which keeps an existing file's permission bits and gives a new file the process default.
173
176
 
174
177
  ### Console Output
175
178
  All console output must go through `ThinkingPromptSession` methods. Never use `rich.Console` or `print()` directly.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: agentic-cli
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: A framework for building domain-specific agentic CLI applications
5
5
  Project-URL: Homepage, https://github.com/shoom1/agentic-cli
6
6
  Project-URL: Repository, https://github.com/shoom1/agentic-cli
@@ -463,7 +463,7 @@ from agentic_cli.workflow.permissions import Capability, EXEMPT
463
463
 
464
464
  @register_tool(
465
465
  category=ToolCategory.NETWORK,
466
- capabilities=[Capability("http.read")],
466
+ capabilities=[Capability("http.read", target="https://db.example.com/search")],
467
467
  description="Search the database for matching records.",
468
468
  )
469
469
  def search_database(query: str, limit: int = 10) -> dict:
@@ -526,6 +526,8 @@ rather than silently picking one by import order.
526
526
  Tool access is gated by the **permission engine** (see the HITL section below). Each registered tool declares what it touches via `capabilities=`:
527
527
 
528
528
  - **`Capability("namespace.action", target_arg="...")`** — e.g. `Capability("filesystem.write", target_arg="path")`, `Capability("http.read", target_arg="url")`, `Capability("shell.exec", target_arg="command")`. The engine resolves `target_arg` against the actual tool-call arguments and matches against rules.
529
+ - **`Capability("namespace.action", target="...")`** — a fixed target, for a tool that always reaches the same resource, e.g. `Capability("http.read", target="https://export.arxiv.org/api/query")`.
530
+ - Filesystem, network (`http.*`) and shell capabilities must name their resource with `target_arg` or `target`. Without one the target is the wildcard: the tool warns at registration, and every approval of it applies to that one call only, because remembering it would approve the capability for every resource.
529
531
  - **`EXEMPT`** — opts the tool out of the engine entirely. Use for tools that need no permission check: pure functions with no side effects, and backend-internal tools (e.g. ADK `transfer_to_agent`, backend state tools).
530
532
 
531
533
  ### Built-in Tools
@@ -719,7 +721,7 @@ from agentic_cli.tools import memory_tools
719
721
 
720
722
  #### HITL (Human-in-the-Loop)
721
723
 
722
- Tool calls are gated by the **permission engine** (`workflow/permissions/`). Each tool declares a list of capabilities (e.g. `filesystem.write(path=...)`); the engine evaluates them against rules from four sources (builtin defaults, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). When no rule matches, the user is prompted with `Allow once` / `Allow for this session` / `Allow always for this project` / `Deny`.
724
+ Tool calls are gated by the **permission engine** (`workflow/permissions/`). Each tool declares a list of capabilities (e.g. `filesystem.write(path=...)`); the engine evaluates them against rules from four sources (builtin defaults, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). When no rule matches, the user is prompted with `Allow once` / `Allow for this session` / `Allow always for this project` / `Deny`. A filesystem grant covers the directory it names (or the directory a file is in), never the filesystem root or a directory above your home.
723
725
 
724
726
  **"Allow always" grants are user-owned, not project-owned.** They persist to
725
727
  `~/.{app_name}/project_grants.json`, keyed by the *resolved project path*, so
@@ -406,7 +406,7 @@ from agentic_cli.workflow.permissions import Capability, EXEMPT
406
406
 
407
407
  @register_tool(
408
408
  category=ToolCategory.NETWORK,
409
- capabilities=[Capability("http.read")],
409
+ capabilities=[Capability("http.read", target="https://db.example.com/search")],
410
410
  description="Search the database for matching records.",
411
411
  )
412
412
  def search_database(query: str, limit: int = 10) -> dict:
@@ -469,6 +469,8 @@ rather than silently picking one by import order.
469
469
  Tool access is gated by the **permission engine** (see the HITL section below). Each registered tool declares what it touches via `capabilities=`:
470
470
 
471
471
  - **`Capability("namespace.action", target_arg="...")`** — e.g. `Capability("filesystem.write", target_arg="path")`, `Capability("http.read", target_arg="url")`, `Capability("shell.exec", target_arg="command")`. The engine resolves `target_arg` against the actual tool-call arguments and matches against rules.
472
+ - **`Capability("namespace.action", target="...")`** — a fixed target, for a tool that always reaches the same resource, e.g. `Capability("http.read", target="https://export.arxiv.org/api/query")`.
473
+ - Filesystem, network (`http.*`) and shell capabilities must name their resource with `target_arg` or `target`. Without one the target is the wildcard: the tool warns at registration, and every approval of it applies to that one call only, because remembering it would approve the capability for every resource.
472
474
  - **`EXEMPT`** — opts the tool out of the engine entirely. Use for tools that need no permission check: pure functions with no side effects, and backend-internal tools (e.g. ADK `transfer_to_agent`, backend state tools).
473
475
 
474
476
  ### Built-in Tools
@@ -662,7 +664,7 @@ from agentic_cli.tools import memory_tools
662
664
 
663
665
  #### HITL (Human-in-the-Loop)
664
666
 
665
- Tool calls are gated by the **permission engine** (`workflow/permissions/`). Each tool declares a list of capabilities (e.g. `filesystem.write(path=...)`); the engine evaluates them against rules from four sources (builtin defaults, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). When no rule matches, the user is prompted with `Allow once` / `Allow for this session` / `Allow always for this project` / `Deny`.
667
+ Tool calls are gated by the **permission engine** (`workflow/permissions/`). Each tool declares a list of capabilities (e.g. `filesystem.write(path=...)`); the engine evaluates them against rules from four sources (builtin defaults, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). When no rule matches, the user is prompted with `Allow once` / `Allow for this session` / `Allow always for this project` / `Deny`. A filesystem grant covers the directory it names (or the directory a file is in), never the filesystem root or a directory above your home.
666
668
 
667
669
  **"Allow always" grants are user-owned, not project-owned.** They persist to
668
670
  `~/.{app_name}/project_grants.json`, keyed by the *resolved project path*, so
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentic-cli"
7
- version = "0.6.0"
7
+ version = "0.6.1"
8
8
  description = "A framework for building domain-specific agentic CLI applications"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -105,4 +105,4 @@ __all__ = [
105
105
  "CLISettingsMixin",
106
106
  ]
107
107
 
108
- __version__ = "0.6.0"
108
+ __version__ = "0.6.1"
@@ -27,6 +27,8 @@ from agentic_cli.logging import Loggers, configure_logging
27
27
  from agentic_cli.settings_persistence import PROJECT_SETTABLE_KEYS
28
28
 
29
29
  if TYPE_CHECKING:
30
+ from collections.abc import Iterable
31
+
30
32
  from agentic_cli.settings_persistence import SettingsSaveResult
31
33
  from agentic_cli.workflow import GoogleADKWorkflowManager, EventType, WorkflowEvent
32
34
  from agentic_cli.workflow.base_manager import BaseWorkflowManager
@@ -279,9 +281,15 @@ class BaseCLIApp:
279
281
  # Sort by order and return items only
280
282
  return [item for _, item in sorted(items, key=lambda x: x[0])]
281
283
 
282
- async def save_settings(self) -> "SettingsSaveResult":
284
+ async def save_settings(
285
+ self, keys: "Iterable[str] | None" = None
286
+ ) -> "SettingsSaveResult":
283
287
  """Save current settings, split by trust.
284
288
 
289
+ ``keys`` restricts the save to those fields (``/settings`` passes the
290
+ fields the user changed). Without it the whole live settings object is
291
+ saved, including values that came from the environment.
292
+
285
293
  Allowlisted keys go to the project config
286
294
  (./.{app_name}/settings.json). User-scoped keys differing from their
287
295
  defaults go to the user config (~/.{app_name}/settings.json), where
@@ -294,7 +302,7 @@ class BaseCLIApp:
294
302
  from agentic_cli.settings_persistence import SettingsPersistence
295
303
 
296
304
  persistence = SettingsPersistence(self._settings.app_name)
297
- return persistence.save(self._settings)
305
+ return persistence.save(self._settings, keys=keys)
298
306
 
299
307
  @property
300
308
  def workflow(self) -> "BaseWorkflowManager":
@@ -330,9 +338,14 @@ class BaseCLIApp:
330
338
  self.should_exit = True
331
339
  self.session.exit()
332
340
 
333
- async def apply_settings(self, changes: dict[str, Any]) -> None:
341
+ async def apply_settings(self, changes: dict[str, Any]) -> bool:
334
342
  """Apply changed settings and reinitialize workflow if needed.
335
343
 
344
+ All or nothing: if any value is invalid, or the workflow cannot be
345
+ reinitialized with the new values, every field is restored and the
346
+ workflow is brought back on the model it was running, so nothing
347
+ unusable stays in memory to be saved by ``/settings``.
348
+
336
349
  Settings are applied in two ways:
337
350
  1. Special settings (model, thinking_effort) use dedicated setters
338
351
  and may trigger workflow reinitialization
@@ -340,13 +353,17 @@ class BaseCLIApp:
340
353
 
341
354
  Args:
342
355
  changes: Dictionary of changed settings (key -> new_value)
356
+
357
+ Returns:
358
+ True if the changes were applied, False if they were rolled back.
343
359
  """
344
360
  if not changes:
345
361
  self.session.add_message("system", "No changes made.")
346
- return
362
+ return True
347
363
 
348
364
  needs_reinit = False
349
365
  new_model = changes.get("model")
366
+ snapshot = _snapshot_settings(self._settings)
350
367
 
351
368
  # `orchestrator` triggers a reinit too: the controller swaps the backend
352
369
  # when it changes (otherwise the change only took effect on restart).
@@ -358,22 +375,40 @@ class BaseCLIApp:
358
375
  if key in reinit_settings:
359
376
  needs_reinit = True
360
377
  except ValueError as e:
378
+ _restore_settings(self._settings, snapshot)
361
379
  label = key.replace("_", " ").title()
362
380
  self.session.add_error(f"Failed to set {label}: {e}")
363
- return
381
+ return False
364
382
 
365
383
  # Reinitialize workflow if needed
366
- if needs_reinit and self._workflow_controller.is_ready:
384
+ controller = self._workflow_controller
385
+ if needs_reinit and controller.is_ready:
386
+ previous_model = controller.workflow.model
367
387
  try:
368
- await self._workflow_controller.reinitialize(model=new_model)
388
+ await controller.reinitialize(model=new_model)
369
389
  except Exception as e:
370
- self.session.add_error(f"Failed to reinitialize workflow: {e}")
371
- return
390
+ _restore_settings(self._settings, snapshot)
391
+ self.session.add_error(
392
+ f"Failed to reinitialize workflow: {e}. Settings were not changed."
393
+ )
394
+ # A failed orchestrator swap leaves the old manager running; a
395
+ # failed in-place reinitialize leaves it down, holding the new
396
+ # model. Bring it back on the model it had.
397
+ if not controller.is_ready:
398
+ try:
399
+ await controller.reinitialize(model=previous_model)
400
+ except Exception as restore_error:
401
+ self.session.add_error(
402
+ f"Could not restart the workflow on {previous_model}: "
403
+ f"{restore_error}"
404
+ )
405
+ return False
372
406
 
373
407
  # Report changes
374
408
  for key, value in changes.items():
375
409
  label = key.replace("_", " ").title()
376
410
  self.session.add_success(f"{label}: {value}")
411
+ return True
377
412
 
378
413
  def _register_builtin_commands(self) -> None:
379
414
  """Register built-in commands."""
@@ -639,3 +674,23 @@ class BaseCLIApp:
639
674
 
640
675
  logger.info("app_ending")
641
676
  self.session.add_message("system", "Goodbye!")
677
+
678
+
679
+ def _snapshot_settings(settings: BaseSettings) -> tuple[dict[str, Any], set[str]]:
680
+ """Capture every field value (and which were set) so a change can be undone."""
681
+ return dict(settings.__dict__), set(settings.__pydantic_fields_set__)
682
+
683
+
684
+ def _restore_settings(
685
+ settings: BaseSettings, snapshot: tuple[dict[str, Any], set[str]]
686
+ ) -> None:
687
+ """Put back the values captured by :func:`_snapshot_settings`.
688
+
689
+ Written directly, like the dedicated setters do: they were valid when
690
+ captured, and revalidating could itself fail.
691
+ """
692
+ values, fields_set = snapshot
693
+ for key, value in values.items():
694
+ if settings.__dict__.get(key) is not value:
695
+ object.__setattr__(settings, key, value)
696
+ object.__setattr__(settings, "__pydantic_fields_set__", fields_set)
@@ -726,9 +726,17 @@ class MessageProcessor:
726
726
  first_line = lines[0]
727
727
  state.status_line = f"{icon} {tool_name}: {first_line}{duration_str}"
728
728
  style = "green" if success else "red"
729
- display = f"[{style}]{icon}[/{style}] {tool_name}: {first_line}{duration_str}"
729
+ # Tool names and output are data: escape them, or a path such as
730
+ # "notes[/draft].md" is parsed as a closing tag (MarkupError, failing
731
+ # the turn after the tool already ran) and "[bold]" text restyles it.
732
+ from rich.markup import escape
733
+
734
+ display = (
735
+ f"[{style}]{icon}[/{style}] {escape(tool_name)}: "
736
+ f"{escape(first_line)}{duration_str}"
737
+ )
730
738
  if len(lines) > 1:
731
- display += "\n" + "\n".join(lines[1:])
739
+ display += "\n" + escape("\n".join(lines[1:]))
732
740
  ui.add_rich(display)
733
741
 
734
742
  async def _handle_code_execution(
@@ -4,6 +4,7 @@ Uses introspection to automatically generate UI controls from Pydantic fields.
4
4
  Apps can customize which settings appear by overriding get_ui_setting_keys().
5
5
  """
6
6
 
7
+ import inspect
7
8
  from typing import TYPE_CHECKING
8
9
 
9
10
  from thinking_prompt import SettingsDialog
@@ -52,17 +53,40 @@ class SettingsCommand(Command):
52
53
  app.session.add_message("system", "Settings unchanged.")
53
54
  return
54
55
 
55
- # Apply settings changes
56
- await app.apply_settings(result)
56
+ # Apply, then persist exactly the fields that changed. Saving the whole
57
+ # live object also wrote values that came from the environment or were
58
+ # derived at startup, making a one-run override permanent.
59
+ before = app.settings.model_dump()
60
+ if await app.apply_settings(result) is False:
61
+ return # rolled back (an override returning None is still diffed)
62
+ after = app.settings.model_dump()
63
+ changed = {k for k, v in after.items() if before.get(k) != v}
64
+ if not changed:
65
+ return
57
66
 
58
- # Save settings, split by trust (allowlisted → project config,
59
- # user-scoped → user config)
60
67
  try:
61
- saved = await app.save_settings()
62
- message = f"Settings saved to {saved.project_path}"
63
- if saved.user_path is not None:
64
- keys = ", ".join(saved.user_scoped_keys)
65
- message += f"; user-scoped ({keys}) saved to {saved.user_path}"
66
- app.session.add_success(message)
68
+ if _accepts_keyword(app.save_settings, "keys"):
69
+ saved = await app.save_settings(keys=changed)
70
+ else: # an app override predating keys=: its own full save
71
+ saved = await app.save_settings()
72
+ app.session.add_success(_saved_message(saved))
67
73
  except Exception as e:
68
74
  app.session.add_warning(f"Settings applied but not saved: {e}")
75
+
76
+
77
+ def _saved_message(saved) -> str:
78
+ """Describe where a save landed."""
79
+ parts = []
80
+ project_keys = getattr(saved, "project_keys", None)
81
+ if project_keys is None or project_keys:
82
+ parts.append(f"Settings saved to {saved.project_path}")
83
+ if saved.user_path is not None:
84
+ keys = ", ".join(saved.user_scoped_keys)
85
+ parts.append(f"user-scoped ({keys}) saved to {saved.user_path}")
86
+ return "; ".join(parts) or "Settings saved"
87
+
88
+
89
+ def _accepts_keyword(func, name: str) -> bool:
90
+ """Whether ``func`` can be called with keyword ``name``."""
91
+ params = inspect.signature(func).parameters.values()
92
+ return any(p.name == name or p.kind is p.VAR_KEYWORD for p in params)
@@ -49,6 +49,7 @@ from agentic_cli.settings_mixins import AppSettingsMixin, CLISettingsMixin
49
49
  # and the load-side filter below share one definition (and because the reverse
50
50
  # import would be circular). See its definition for the trust rationale.
51
51
  from agentic_cli.settings_persistence import (
52
+ CREDENTIAL_KEY_RE as _CREDENTIAL_KEY_RE,
52
53
  PROJECT_SETTABLE_KEYS as _PROJECT_SETTABLE_KEYS,
53
54
  get_project_config_path,
54
55
  get_user_config_path,
@@ -107,9 +108,10 @@ class _AllowlistFilterSource(PydanticBaseSettingsSource):
107
108
  return kept
108
109
 
109
110
 
110
- # Constructor kwargs matching this shape are credentials; an unrecognised one
111
- # must fail loudly instead of being swallowed by ``extra="ignore"``.
112
- _CREDENTIAL_KEY_RE = re.compile(r"(?i)(api_?key|secret|token|password|credential)")
111
+ # Constructor kwargs matching _CREDENTIAL_KEY_RE (defined in
112
+ # settings_persistence, which also uses it to never save credentials) are
113
+ # credentials; an unrecognised one must fail loudly instead of being swallowed
114
+ # by ``extra="ignore"``.
113
115
 
114
116
 
115
117
  def _accepted_input_names(settings_cls: Type[PydanticBaseSettings]) -> set[str]:
@@ -4,6 +4,7 @@ import fcntl
4
4
  import json
5
5
  import os
6
6
  import re
7
+ import secrets
7
8
  import stat
8
9
  import tempfile
9
10
  import time
@@ -128,25 +129,54 @@ def copy_regular_file_no_follow(src: Path, dst: Path) -> None:
128
129
  os.close(fd)
129
130
 
130
131
 
131
- def _atomic_write(path: Path, content: str) -> None:
132
+ def _open_temp_like_a_new_file(path: Path) -> tuple[int, Path]:
133
+ """Create a uniquely-named temp file beside ``path``, opened for writing,
134
+ with the mode a plain ``open()`` would give a new file (0666 minus the
135
+ umask: the kernel applies it). ``mkstemp`` would force 0600."""
136
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
137
+ for _ in range(100):
138
+ tmp_path = path.parent / f".{path.name}.{secrets.token_hex(6)}.tmp"
139
+ try:
140
+ return os.open(tmp_path, flags, 0o666), tmp_path
141
+ except FileExistsError:
142
+ continue
143
+ raise FileExistsError(f"could not create a temp file beside {path}")
144
+
145
+
146
+ def _atomic_write(path: Path, content: str, *, preserve_mode: bool = False) -> None:
132
147
  """Write content to a file atomically and durably.
133
148
 
134
149
  Writes to a uniquely-named temp file in the same directory, flushes and
135
150
  fsyncs it, then atomically renames it over the target. Notes:
136
- - Unique temp name (tempfile.mkstemp) so two concurrent writers can't
137
- truncate each other's temp file the way a fixed ``.tmp`` name allows.
151
+ - Unique temp name so two concurrent writers can't truncate each other's
152
+ temp file the way a fixed ``.tmp`` name allows.
138
153
  - fsync before the rename so a crash can't persist the rename ahead of the
139
154
  data and leave a torn/empty file in place of the previously-good one.
140
155
  - Explicit UTF-8 so output doesn't depend on the locale (a C/POSIX locale
141
156
  would otherwise raise UnicodeEncodeError on non-ASCII content).
157
+ - Permissions: by default the file is private (0600), right for the
158
+ framework's own state. ``preserve_mode=True`` is for files the user
159
+ owns: an existing file keeps its permission bits and a new one gets the
160
+ process default, as with a plain write. Setuid/setgid are not carried
161
+ over (the kernel clears them when a non-root process writes a file).
142
162
  """
143
163
  path.parent.mkdir(parents=True, exist_ok=True)
144
- fd, tmp_name = tempfile.mkstemp(
145
- dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
146
- )
147
- tmp_path = Path(tmp_name)
164
+ existing = None
165
+ if preserve_mode:
166
+ try:
167
+ existing = stat.S_IMODE(os.stat(path).st_mode) & 0o777
168
+ except FileNotFoundError:
169
+ pass
170
+ fd, tmp_path = _open_temp_like_a_new_file(path)
171
+ else:
172
+ fd, tmp_name = tempfile.mkstemp(
173
+ dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
174
+ )
175
+ tmp_path = Path(tmp_name)
148
176
  try:
149
177
  with os.fdopen(fd, "w", encoding="utf-8") as f:
178
+ if existing is not None:
179
+ os.fchmod(f.fileno(), existing)
150
180
  f.write(content)
151
181
  f.flush()
152
182
  os.fsync(f.fileno())
@@ -161,6 +191,10 @@ def atomic_write_json(path: Path, data: Any, indent: int = 2) -> None:
161
191
  _atomic_write(path, json.dumps(data, indent=indent))
162
192
 
163
193
 
164
- def atomic_write_text(path: Path, content: str) -> None:
165
- """Write text to a file atomically."""
166
- _atomic_write(path, content)
194
+ def atomic_write_text(path: Path, content: str, *, preserve_mode: bool = False) -> None:
195
+ """Write text to a file atomically.
196
+
197
+ The file is private (0600) unless ``preserve_mode``: then an existing
198
+ file keeps its permission bits and a new one gets the process default.
199
+ """
200
+ _atomic_write(path, content, preserve_mode=preserve_mode)
@@ -0,0 +1,35 @@
1
+ """The single interpretation of a filesystem path argument.
2
+
3
+ A tool's path argument is interpreted twice: once by the permission engine
4
+ (to decide) and once by the tool (to act). If the two interpretations ever
5
+ differ, the engine authorizes one file while the tool touches another. Both
6
+ therefore call :func:`resolve_path`, and nothing else.
7
+
8
+ The interpretation:
9
+
10
+ * ``~`` expands to the user's home directory.
11
+ * A relative path is anchored to the process's current directory, which is
12
+ what the tool's own filesystem calls use.
13
+ * ``..`` and symlinks are resolved (``Path.resolve(strict=False)``), so the
14
+ result is the real location, whether or not it exists yet.
15
+ * ``${workdir}``-style placeholders are **not** expanded. They belong to rule
16
+ patterns (see ``workflow.permissions``), never to arguments; in an argument
17
+ they are ordinary characters in a directory name.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from pathlib import Path
24
+
25
+ __all__ = ["resolve_path"]
26
+
27
+
28
+ def resolve_path(raw: str | os.PathLike[str]) -> Path:
29
+ """Resolve a path argument to the absolute location a tool will act on.
30
+
31
+ Raises:
32
+ ValueError: if ``raw`` cannot name a path (for example it contains a
33
+ NUL byte).
34
+ """
35
+ return Path(raw).expanduser().resolve(strict=False)