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.
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/.github/workflows/ci.yml +32 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/CHANGELOG.md +39 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/CLAUDE.md +5 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/PKG-INFO +6 -4
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/README.md +4 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/pyproject.toml +1 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/__init__.py +1 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/app.py +64 -9
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/message_processor.py +10 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/settings_command.py +34 -10
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/config.py +5 -3
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/file_utils.py +44 -10
- agentic_cli-0.6.1/src/agentic_cli/paths.py +35 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/settings_persistence.py +117 -23
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/__init__.py +27 -6
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/arxiv_source.py +32 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/arxiv_tools.py +8 -3
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/document/compile.py +71 -30
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/execution_tools.py +4 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/executor.py +14 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/factories.py +6 -5
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/file_read.py +31 -10
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/file_write.py +18 -12
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/glob_tool.py +2 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/grep_tool.py +2 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/interaction_tools.py +11 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/tools.py +5 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/knowledge_tools.py +2 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/memory_tools.py +11 -4
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/registry.py +16 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/__init__.py +9 -4
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/jupyter_docker.py +6 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/jupyter_local.py +41 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/kernel_exec.py +29 -13
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/manager.py +14 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/search.py +4 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/bubblewrap.py +1 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/policy.py +72 -16
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/seatbelt.py +1 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/tool_resolver.py +30 -6
- agentic_cli-0.6.1/src/agentic_cli/tools/webfetch/_text.py +18 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/fetcher.py +14 -3
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/robots.py +3 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/__init__.py +7 -1
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/manager.py +4 -1
- agentic_cli-0.6.1/src/agentic_cli/workflow/adk/tool_error_plugin.py +59 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/base_manager.py +12 -6
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/events.py +9 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/permission_wrap.py +8 -5
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/__init__.py +2 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/capabilities.py +27 -2
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/engine.py +96 -22
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/matchers.py +56 -8
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/prompt.py +22 -11
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/store.py +17 -2
- agentic_cli-0.6.1/tests/cli/test_settings_apply_rollback.py +178 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_permission_langgraph.py +95 -0
- agentic_cli-0.6.1/tests/permissions/test_capability_targets.py +236 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_engine.py +28 -26
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_matchers.py +91 -11
- agentic_cli-0.6.1/tests/permissions/test_no_approver.py +148 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_prompt.py +7 -6
- agentic_cli-0.6.1/tests/permissions/test_target_resolution.py +205 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_arxiv_download_security.py +5 -3
- agentic_cli-0.6.1/tests/test_google_search_deprecation.py +155 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_memory.py +21 -6
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_message_processor_render.py +67 -0
- agentic_cli-0.6.1/tests/test_paths.py +68 -0
- agentic_cli-0.6.1/tests/test_settings_save_changes.py +165 -0
- agentic_cli-0.6.1/tests/tools/test_capability_declarations.py +134 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_document_compile.py +160 -45
- agentic_cli-0.6.1/tests/tools/test_document_compile_latex.py +140 -0
- agentic_cli-0.6.1/tests/tools/test_file_mode.py +84 -0
- agentic_cli-0.6.1/tests/tools/test_local_kernel_timeout.py +139 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_os_sandbox.py +9 -6
- agentic_cli-0.6.1/tests/tools/test_os_sandbox_protected.py +190 -0
- agentic_cli-0.6.1/tests/tools/test_path_contract.py +211 -0
- agentic_cli-0.6.1/tests/tools/test_python_exec_timeouts.py +119 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_docker_backend.py +2 -2
- agentic_cli-0.6.1/tests/tools/test_sandbox_session_ids.py +56 -0
- agentic_cli-0.6.1/tests/tools/test_tool_declarations.py +55 -0
- agentic_cli-0.6.1/tests/tools/test_tools_return_errors.py +214 -0
- agentic_cli-0.6.1/tests/tools/test_webfetch_status_charset.py +135 -0
- agentic_cli-0.6.0/tests/tools/test_document_compile_latex.py +0 -37
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/.gitignore +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/LICENSE +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/environment.yml +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/arxiv_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/fileops_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/hello_agent.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/hello_langgraph.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/jobs_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/memory_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/__main__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/agents.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/app.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/commands.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/data/benchmarks.csv +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/settings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/skills/report-writer/SKILL.md +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/research_demo/skills/report-writer/assets/report_template.tex +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/shell_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/webfetch_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/examples/websearch_demo.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/requirements-dev.txt +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/requirements.txt +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/builtin_commands.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/commands.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/job_monitor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/sandbox_render.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/settings_introspection.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/usage_tracker.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/cli/workflow_controller.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/constants.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/_bm25_backends.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/_mock_bm25.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/_mocks.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/bm25_index.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/concepts.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/embeddings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/manager.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/models.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/sidecar.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/sources.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/knowledge_base/vector_store.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/logging.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/persistence/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/settings_mixins.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/planning.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/state_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/_core/tasks.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/adk/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/adk/state_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/document/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/backends.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/jobs/manager.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/langgraph/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/langgraph/state_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/pdf_utils.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/base.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/container_runtime.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/detect.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/backends/driver.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/sandbox/models.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/audit.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/classifier.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/config.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/executor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/models.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/base.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/detect.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/os_sandbox/seatbelt_profile.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/path_analyzer.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/preprocessor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/risk_assessor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/sandbox.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/shell/tokenizer.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/permissions.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/store.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/skills/toolset.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/converter.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/summarizer.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/transport.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch/validator.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/tools/webfetch_tool.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/anthropic_llm.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/event_processor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/permission_plugin.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/plugins.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/task_progress_plugin.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk/transfer_tool_description.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/adk_config_bridge.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/agent_loader.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/config.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/factory.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/graph_builder.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/manager.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/persistence/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/persistence/stores.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/langgraph/state.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/mcp.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/model_settings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/models.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/permissions/rules.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/retry.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/service_registry.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/sessions.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/settings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/src/agentic_cli/workflow/tool_summaries.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_job_monitor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_process_resume.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_resume_coordinator.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_sandbox_render.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_sessions_command.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_settings_ui_scope.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_turn_boundary.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/cli/test_turn_retry_safety.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/conftest.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/demo_isolation.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/event_replay.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/console_smoke.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_report_writer_skill.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_agents.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_app.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_pty.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/examples/test_research_demo_wheel.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/conftest.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/helpers.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_adk_claude_live.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_adk_integration.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_kb_research_flow.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_langgraph_integration.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_live.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_live_durable_sessions.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_live_job_resume.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_permission_adk.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/integration/test_research_scenarios.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_capabilities.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_grant_provenance.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_rules.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_store.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/permissions/test_trust_model.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_adk_event_processor.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_adk_plugins.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_arxiv_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_bm25_index.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_commands.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_concept_store.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_concurrent_stores.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_config.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_config_trust.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_context_trimming.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_context_window.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_dual_thinking_boxes.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_embeddings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_executor_security.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_file_utils_atomic.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_file_utils_nofollow.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_grep_tool_cache.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_grep_tool_security.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_import_surface.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_input_callback.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_concepts.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_helpers.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_index_md.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_ingest_log.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_sidecar.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_kb_tool_renames.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_knowledge_base.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_knowledge_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_langgraph.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_memory_thread_safety.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_message_processor_cancel.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_model_registry.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_packaging.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_planning.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_retry.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_settings_persistence.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_task_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_token_caching.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_tool_summaries.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_update_setting.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_usage_tracker.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_vector_store.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_webfetch.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_workflow.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/test_workflow_controller.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_adk_state_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_bubblewrap.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_core_planning.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_core_tasks.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_executor_security.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_factories.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_glob_grep_containment.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_jobs.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_langgraph_state_tools.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_registry.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_registry_identity.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_container_runtime.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_container_session.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_detect.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_docker_integration.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_docker_live.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_driver.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_kernel_exec.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_settings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_sandbox_transfer.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_seatbelt_profile.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_shell_security.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_state_tool_aliases.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/tools/test_webfetch_ssrf.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/__init__.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_active_turn_context.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_agent_construction.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_claude.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_claude_adaptive.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_config_bridge.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_direct_anthropic.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_job_resume.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_mcp_permissions.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_adk_transfer_tool.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_agent_graph.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_agent_loader.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_backend_isolation.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_base_manager_init_lock.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_base_manager_permissions.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_langgraph_ignored_fields.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_lifecycle_races.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_mcp_config.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_memory_wiring.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_model_settings.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_model_validation.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_permission_tool_identity.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_resource_ownership.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_session_store.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_settings_permissions.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_skills.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_task_progress_plugin.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_thinking_config.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_tool_assembly.py +0 -0
- {agentic_cli-0.6.0 → agentic_cli-0.6.1}/tests/workflow/test_tool_resolver.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: agentic-cli
|
|
3
|
-
Version: 0.6.
|
|
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
|
|
@@ -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(
|
|
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]) ->
|
|
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
|
-
|
|
384
|
+
controller = self._workflow_controller
|
|
385
|
+
if needs_reinit and controller.is_ready:
|
|
386
|
+
previous_model = controller.workflow.model
|
|
367
387
|
try:
|
|
368
|
-
await
|
|
388
|
+
await controller.reinitialize(model=new_model)
|
|
369
389
|
except Exception as e:
|
|
370
|
-
self.
|
|
371
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
111
|
-
#
|
|
112
|
-
|
|
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
|
|
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
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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)
|