tsugite-cli 0.23.0__tar.gz → 0.23.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 (182) hide show
  1. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/PKG-INFO +1 -1
  2. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/pyproject.toml +1 -1
  3. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/approval.py +17 -10
  4. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/record_store.py +10 -2
  5. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/reconstruction.py +22 -0
  6. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/permissions.py +23 -11
  7. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/http.py +20 -3
  8. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/.gitignore +0 -0
  9. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/AGENTS.md +0 -0
  10. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/CONTRIBUTING.md +0 -0
  11. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/LICENSE +0 -0
  12. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/README.md +0 -0
  13. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/__init__.py +0 -0
  14. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_inheritance.py +0 -0
  15. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_preparation.py +0 -0
  16. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/__init__.py +0 -0
  17. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/exec_directives.py +0 -0
  18. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/exec_runner.py +0 -0
  19. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/helpers.py +0 -0
  20. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/history_integration.py +0 -0
  21. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/metrics.py +0 -0
  22. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/models.py +0 -0
  23. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/runner.py +0 -0
  24. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/steps.py +0 -0
  25. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_runner/validation.py +0 -0
  26. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/agent_utils.py +0 -0
  27. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/__init__.py +0 -0
  28. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/agent_config.py +0 -0
  29. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/auto_context.py +0 -0
  30. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/base.py +0 -0
  31. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/delegation.py +0 -0
  32. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/file.py +0 -0
  33. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/inline.py +0 -0
  34. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/storage.py +0 -0
  35. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/attachments/url.py +0 -0
  36. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/.gitkeep +0 -0
  37. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/code_searcher.md +0 -0
  38. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/default.md +0 -0
  39. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/file_searcher.md +0 -0
  40. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/job_verifier.md +0 -0
  41. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/job_worker.md +0 -0
  42. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_agents/onboard.md +0 -0
  43. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_context.py +0 -0
  44. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/.gitkeep +0 -0
  45. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  46. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  47. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  48. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  49. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/scheduling/SKILL.md +0 -0
  50. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  51. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  52. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  53. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  54. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cache.py +0 -0
  55. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cancellation.py +0 -0
  56. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/__init__.py +0 -0
  57. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/agents.py +0 -0
  58. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/attachments.py +0 -0
  59. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/cache.py +0 -0
  60. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/chat.py +0 -0
  61. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/config.py +0 -0
  62. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/daemon.py +0 -0
  63. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/exec.py +0 -0
  64. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/helpers.py +0 -0
  65. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/history.py +0 -0
  66. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/init.py +0 -0
  67. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/models.py +0 -0
  68. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/plugins.py +0 -0
  69. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/render.py +0 -0
  70. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/run.py +0 -0
  71. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/secrets.py +0 -0
  72. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/skills.py +0 -0
  73. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/tools.py +0 -0
  74. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/usage.py +0 -0
  75. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/validate.py +0 -0
  76. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/cli/workspace.py +0 -0
  77. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/config.py +0 -0
  78. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/console.py +0 -0
  79. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/constants.py +0 -0
  80. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/context.py +0 -0
  81. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/context_block.py +0 -0
  82. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/__init__.py +0 -0
  83. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/agent.py +0 -0
  84. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/content_blocks.py +0 -0
  85. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/executor.py +0 -0
  86. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/executor_registry.py +0 -0
  87. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/memory.py +0 -0
  88. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/proxy.py +0 -0
  89. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/sandbox.py +0 -0
  90. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/state.py +0 -0
  91. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/subprocess_executor.py +0 -0
  92. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/core/tools.py +0 -0
  93. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/events/__init__.py +0 -0
  94. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/events/base.py +0 -0
  95. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/events/bus.py +0 -0
  96. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/events/events.py +0 -0
  97. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/events/helpers.py +0 -0
  98. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/exceptions.py +0 -0
  99. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/__init__.py +0 -0
  100. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/base.py +0 -0
  101. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/models.py +0 -0
  102. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/registry.py +0 -0
  103. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/sqlite_backend.py +0 -0
  104. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/sqlite_conn.py +0 -0
  105. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/sqlite_schema.py +0 -0
  106. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/history/ui_events.py +0 -0
  107. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/hooks.py +0 -0
  108. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/interaction.py +0 -0
  109. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/md_agents.py +0 -0
  110. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/models.py +0 -0
  111. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/options.py +0 -0
  112. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/plugins.py +0 -0
  113. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/prompt_xml.py +0 -0
  114. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/__init__.py +0 -0
  115. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/anthropic.py +0 -0
  116. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/base.py +0 -0
  117. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/model_cache.py +0 -0
  118. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/model_registry.py +0 -0
  119. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/ollama.py +0 -0
  120. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/openai_compat.py +0 -0
  121. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/providers/openrouter.py +0 -0
  122. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/py.typed +0 -0
  123. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/renderer.py +0 -0
  124. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/schemas/__init__.py +0 -0
  125. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/schemas/agent.schema.json +0 -0
  126. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/__init__.py +0 -0
  127. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/backend.py +0 -0
  128. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/env.py +0 -0
  129. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/exec.py +0 -0
  130. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/file.py +0 -0
  131. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/masking.py +0 -0
  132. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/redaction.py +0 -0
  133. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/registry.py +0 -0
  134. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/secrets/sqlite.py +0 -0
  135. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/shell_tool_config.py +0 -0
  136. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/skill_discovery.py +0 -0
  137. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/AGENTS.md +0 -0
  138. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/IDENTITY.md +0 -0
  139. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/MEMORY.md +0 -0
  140. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/USER.md +0 -0
  141. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/personas/casual-technical.md +0 -0
  142. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/personas/marvin.md +0 -0
  143. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/templates/personas/minimal.md +0 -0
  144. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/__init__.py +0 -0
  145. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/agents.py +0 -0
  146. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/artifacts.py +0 -0
  147. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/daemon_control.py +0 -0
  148. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/fs.py +0 -0
  149. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/history.py +0 -0
  150. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/interactive.py +0 -0
  151. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/jobs.py +0 -0
  152. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/notify.py +0 -0
  153. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/schedule.py +0 -0
  154. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/secrets.py +0 -0
  155. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/sessions.py +0 -0
  156. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/shell.py +0 -0
  157. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/shell_tools.py +0 -0
  158. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/skills.py +0 -0
  159. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tools/time.py +0 -0
  160. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/tsugite.py +0 -0
  161. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/__init__.py +0 -0
  162. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/base.py +0 -0
  163. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/chat.py +0 -0
  164. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/dispatch.py +0 -0
  165. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/helpers.py +0 -0
  166. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/jsonl.py +0 -0
  167. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/plain.py +0 -0
  168. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/repl_chat.py +0 -0
  169. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/repl_commands.py +0 -0
  170. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/repl_completer.py +0 -0
  171. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui/repl_handler.py +0 -0
  172. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui_context.py +0 -0
  173. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/ui_surfaces.py +0 -0
  174. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/usage/__init__.py +0 -0
  175. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/usage/quota.py +0 -0
  176. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/usage/store.py +0 -0
  177. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/user_agent.py +0 -0
  178. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/utils.py +0 -0
  179. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/workspace/__init__.py +0 -0
  180. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/workspace/models.py +0 -0
  181. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/workspace/session.py +0 -0
  182. {tsugite_cli-0.23.0 → tsugite_cli-0.23.1}/tsugite/workspace/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tsugite-cli
3
- Version: 0.23.0
3
+ Version: 0.23.1
4
4
  Summary: Micro-agent runner for task automation using markdown definitions
5
5
  Author: Justyn Shull
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.23.0"
3
+ version = "0.23.1"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -1,9 +1,9 @@
1
1
  """Backend-driven approval primitive.
2
2
 
3
3
  A human-in-the-loop gate that pauses a turn before an action and asks the user
4
- to approve or deny it. It rides the same InteractionBackend used by ``ask_user``,
5
- so an approval surfaces on whatever adapter the user is on (CLI prompt, web SSE
6
- frame, etc.).
4
+ to approve or deny it, once or persistently. It rides the same InteractionBackend
5
+ used by ``ask_user``, so an approval surfaces on whatever adapter the user is on
6
+ (CLI prompt, web SSE frame, etc.).
7
7
 
8
8
  Fail-closed by design: when no interactive surface is available (no backend, or a
9
9
  non-interactive scheduler/headless backend), or when the reply is unrecognized,
@@ -15,34 +15,39 @@ from typing import Literal, Optional
15
15
 
16
16
  from tsugite.interaction import NonInteractiveBackend, get_interaction_backend
17
17
 
18
- ApprovalDecision = Literal["approve", "deny", "always"]
18
+ ApprovalDecision = Literal["approve", "deny", "always", "never"]
19
19
 
20
20
  APPROVE_LABEL = "Approve"
21
21
  DENY_LABEL = "Deny"
22
22
  ALWAYS_LABEL = "Always allow"
23
+ NEVER_LABEL = "Always deny"
23
24
 
24
25
  _LABEL_TO_DECISION = {
25
26
  APPROVE_LABEL: "approve",
26
27
  DENY_LABEL: "deny",
27
28
  ALWAYS_LABEL: "always",
29
+ NEVER_LABEL: "never",
28
30
  }
29
31
 
30
32
 
31
- def request_approval(prompt: str, *, allow_always: bool = False, detail: Optional[str] = None) -> str:
33
+ def request_approval(
34
+ prompt: str, *, allow_always: bool = False, allow_never: bool = False, detail: Optional[str] = None
35
+ ) -> str:
32
36
  """Ask the user to approve an action, returning an :data:`ApprovalDecision`.
33
37
 
34
- Builds ``Approve``/``Deny`` options (plus ``Always allow`` when
35
- ``allow_always``) and routes them through the active interaction backend as an
36
- ``"approval"`` question, then maps the chosen label back to ``"approve"``,
37
- ``"deny"``, or ``"always"``.
38
+ Builds ``Approve``/``Deny`` options (plus ``Always allow`` when ``allow_always``
39
+ and ``Always deny`` when ``allow_never``) and routes them through the active
40
+ interaction backend as an ``"approval"`` question, then maps the chosen label
41
+ back to a decision.
38
42
 
39
43
  Args:
40
44
  prompt: The action to approve, phrased as a question.
41
45
  allow_always: Offer an "Always allow" option (a persistent allow).
46
+ allow_never: Offer an "Always deny" option (a persistent deny).
42
47
  detail: Extra context appended below the prompt for the user to read.
43
48
 
44
49
  Returns:
45
- One of ``"approve"``, ``"deny"``, or ``"always"``.
50
+ One of ``"approve"``, ``"deny"``, ``"always"``, or ``"never"``.
46
51
 
47
52
  Fail-closed: returns ``"deny"`` WITHOUT prompting when there is no backend or
48
53
  the backend is non-interactive, and for any unrecognized reply.
@@ -54,6 +59,8 @@ def request_approval(prompt: str, *, allow_always: bool = False, detail: Optiona
54
59
  options = [APPROVE_LABEL, DENY_LABEL]
55
60
  if allow_always:
56
61
  options.append(ALWAYS_LABEL)
62
+ if allow_never:
63
+ options.append(NEVER_LABEL)
57
64
 
58
65
  question = f"{prompt}\n\n{detail}" if detail else prompt
59
66
  answer = backend.ask_user(question, "approval", options)
@@ -201,6 +201,7 @@ class RecordStore:
201
201
  valid_transitions: dict[str, frozenset[str]]
202
202
  terminal_states: frozenset[str]
203
203
  transition_error_cls: type[ValueError] = ValueError
204
+ missing_error_cls: type[LookupError] = KeyError
204
205
 
205
206
  def __init__(self, path: Path):
206
207
  self._path = path # legacy JSON location; migration source only
@@ -237,11 +238,18 @@ class RecordStore:
237
238
  with self._lock:
238
239
  return [r for r in self._records.values() if r.parent_session_id == parent_session_id]
239
240
 
241
+ def require(self, record_id: str):
242
+ """Raises `missing_error_cls` when no record has that id."""
243
+ record = self.get(record_id)
244
+ if record is None:
245
+ raise self.missing_error_cls(f"Unknown {self.record_label}: {record_id}")
246
+ return record
247
+
240
248
  def update_state(self, record_id: str, new_state: str):
241
249
  with self._lock:
242
250
  record = self._records.get(record_id)
243
251
  if record is None:
244
- raise KeyError(f"Unknown {self.record_label}: {record_id}")
252
+ raise self.missing_error_cls(f"Unknown {self.record_label}: {record_id}")
245
253
  allowed = self.valid_transitions.get(record.state, frozenset())
246
254
  if new_state not in allowed:
247
255
  raise self.transition_error_cls(
@@ -270,7 +278,7 @@ class RecordStore:
270
278
  with self._lock:
271
279
  record = self._records.get(record_id)
272
280
  if record is None:
273
- raise KeyError(f"Unknown {self.record_label}: {record_id}")
281
+ raise self.missing_error_cls(f"Unknown {self.record_label}: {record_id}")
274
282
  for key, value in fields.items():
275
283
  if not hasattr(record, key):
276
284
  raise ValueError(f"Unknown {self.record_cls.__name__} field: {key}")
@@ -11,6 +11,7 @@ from datetime import datetime
11
11
  from typing import Any, Dict, Iterable, List, Optional
12
12
  from xml.sax.saxutils import escape, quoteattr
13
13
 
14
+ from tsugite.prompt_xml import El
14
15
  from tsugite.renderer import format_prompt_ts, parse_iso_utc
15
16
 
16
17
  from .models import Event
@@ -176,6 +177,11 @@ def _event_to_message(event: Event, executed: bool = False) -> Optional[Dict[str
176
177
  return {"role": "user", "content": _format_error_xml(event.data)}
177
178
  if event.type == "delivery":
178
179
  return {"role": "user", "content": _delivery_xml(event.data, event.ts)}
180
+ if event.type == "error":
181
+ # send_failed is a notification that never left the daemon, not a turn failure.
182
+ if event.data.get("kind") == "send_failed":
183
+ return None
184
+ return {"role": "user", "content": _turn_failed_xml(event.data, event.ts)}
179
185
  return None
180
186
 
181
187
 
@@ -223,6 +229,22 @@ def _delivery_xml(data: Dict[str, Any], ts: Optional[datetime] = None) -> str:
223
229
  return "\n".join(parts)
224
230
 
225
231
 
232
+ _TURN_FAILED_NOTE = (
233
+ "The previous attempt failed before it finished. Continue from where it stopped rather than starting over."
234
+ )
235
+
236
+
237
+ def _turn_failed_xml(data: Dict[str, Any], ts: datetime) -> str:
238
+ return El(
239
+ "tsugite_turn_failed",
240
+ [
241
+ El("note", [_TURN_FAILED_NOTE], inline=True),
242
+ El("error", [data.get("error")], inline=True),
243
+ ],
244
+ {"ts": _format_event_ts(ts)},
245
+ ).render()
246
+
247
+
226
248
  def _format_error_xml(data: Dict[str, Any]) -> str:
227
249
  reason = data.get("reason", "")
228
250
  msg = (
@@ -3,23 +3,26 @@
3
3
  Merges two YAML sources, the same way hooks does:
4
4
 
5
5
  1. ``runtime_path``: a mutable ``permissions.yaml`` next to ``daemon.yaml``.
6
- "Always allow" decisions append here, so they survive a restart.
6
+ "Always allow" and "Always deny" decisions append here, so they survive a restart.
7
7
  2. ``workspace_dir``: an optional, read-only ``<workspace_dir>/.tsugite/permissions.yaml``,
8
8
  hand-authored per agent (exactly like ``.tsugite/hooks.yaml``). The store never
9
9
  writes it.
10
10
 
11
- ``is_allowed`` is the union of both files. ``allow`` only ever writes the runtime
11
+ ``is_listed`` is the union of both files. ``add`` only ever writes the runtime
12
12
  file, atomically, preserving every unrelated section.
13
13
 
14
- The file groups allowlists by section, e.g.::
14
+ The file groups lists by section, e.g.::
15
15
 
16
16
  web:
17
17
  fetch_allowlist:
18
18
  - example.com
19
+ fetch_denylist:
20
+ - ads.example.com
19
21
 
20
- The generic ``is_allowed(section, key)`` / ``allow(section, key)`` address a list
21
- by dotted path (``"web.fetch_allowlist"``). The ``web_fetch_*`` wrappers hide that
22
- layout so callers stay decoupled from it.
22
+ The generic ``is_listed(section, key)`` / ``add(section, key)`` address a list by
23
+ dotted path (``"web.fetch_allowlist"``). The ``web_fetch_*`` wrappers hide that
24
+ layout so callers stay decoupled from it. The store applies no precedence between
25
+ lists. The caller decides.
23
26
  """
24
27
 
25
28
  import contextvars
@@ -31,6 +34,7 @@ import yaml
31
34
  from tsugite.utils import atomic_write_text
32
35
 
33
36
  WEB_FETCH_ALLOWLIST = "web.fetch_allowlist"
37
+ WEB_FETCH_DENYLIST = "web.fetch_denylist"
34
38
 
35
39
 
36
40
  def _load_yaml_mapping(path: Optional[Path]) -> dict:
@@ -70,20 +74,20 @@ def _atomic_write_yaml(path: Path, data: dict) -> None:
70
74
 
71
75
 
72
76
  class Permissions:
73
- """Union of a mutable runtime allowlist and a read-only workspace allowlist."""
77
+ """Union of a mutable runtime permissions file and a read-only workspace one."""
74
78
 
75
79
  def __init__(self, runtime_path: Path, workspace_dir: Optional[Path] = None):
76
80
  self.runtime_path = Path(runtime_path)
77
81
  self._workspace_path = (Path(workspace_dir) / ".tsugite" / "permissions.yaml") if workspace_dir else None
78
82
 
79
- def is_allowed(self, section: str, key: str) -> bool:
83
+ def is_listed(self, section: str, key: str) -> bool:
80
84
  """True if ``key`` is listed at dotted ``section`` in EITHER source."""
81
85
  parts = section.split(".")
82
86
  return key in _get_list(_load_yaml_mapping(self.runtime_path), parts) or key in _get_list(
83
87
  _load_yaml_mapping(self._workspace_path), parts
84
88
  )
85
89
 
86
- def allow(self, section: str, key: str) -> None:
90
+ def add(self, section: str, key: str) -> None:
87
91
  """Append ``key`` to the RUNTIME file's list at dotted ``section``.
88
92
 
89
93
  Only ever touches the runtime file (never the workspace file), writes
@@ -102,11 +106,19 @@ class Permissions:
102
106
 
103
107
  def web_fetch_allowed(self, domain: str) -> bool:
104
108
  """True if ``domain`` is on the web fetch allowlist (either source)."""
105
- return self.is_allowed(WEB_FETCH_ALLOWLIST, domain.lower())
109
+ return self.is_listed(WEB_FETCH_ALLOWLIST, domain.lower())
106
110
 
107
111
  def web_fetch_allow(self, domain: str) -> None:
108
112
  """Persist ``domain`` to the runtime web fetch allowlist."""
109
- self.allow(WEB_FETCH_ALLOWLIST, domain.lower())
113
+ self.add(WEB_FETCH_ALLOWLIST, domain.lower())
114
+
115
+ def web_fetch_denied(self, domain: str) -> bool:
116
+ """True if ``domain`` is on the web fetch denylist (either source)."""
117
+ return self.is_listed(WEB_FETCH_DENYLIST, domain.lower())
118
+
119
+ def web_fetch_deny(self, domain: str) -> None:
120
+ """Persist ``domain`` to the runtime web fetch denylist."""
121
+ self.add(WEB_FETCH_DENYLIST, domain.lower())
110
122
 
111
123
 
112
124
  _permissions_var: contextvars.ContextVar[Optional[Permissions]] = contextvars.ContextVar("permissions", default=None)
@@ -18,6 +18,11 @@ _WEB_EXTRA_HINT = "Install it with: pip install 'tsugite-cli[web]'"
18
18
  # reach anything the original request couldn't.
19
19
  _MAX_REDIRECTS = 5
20
20
 
21
+ # readability emits a near-empty node when it finds no article. An extraction that is
22
+ # both short and a sliver of the whole page means extraction failed, not a short article.
23
+ _MIN_ARTICLE_CHARS = 200
24
+ _ARTICLE_PAGE_RATIO = 10
25
+
21
26
 
22
27
  def _extract_article(html: str) -> str:
23
28
  """Extract the main article body from HTML using readability (optional dep)."""
@@ -150,9 +155,21 @@ def fetch_text(
150
155
  if "text/html" not in content_type or (not strip_html and not extract_article):
151
156
  return text
152
157
 
153
- if extract_article:
154
- text = _extract_article(text)
155
- return convert_html_to_markdown(text)
158
+ if not extract_article:
159
+ return convert_html_to_markdown(text)
160
+
161
+ article = convert_html_to_markdown(_extract_article(text))
162
+ article_len = len(article.strip())
163
+ if article_len >= _MIN_ARTICLE_CHARS:
164
+ return article
165
+
166
+ page = convert_html_to_markdown(text)
167
+ page_len = len(page.strip())
168
+ if not page_len:
169
+ raise RuntimeError(f"{url} has no readable text; the page is likely rendered client-side")
170
+ if article_len * _ARTICLE_PAGE_RATIO < page_len:
171
+ return page
172
+ return article
156
173
 
157
174
 
158
175
  # No sensitive_args: the built-in key rules already redact Authorization,
File without changes
File without changes
File without changes
File without changes