tsugite-cli 0.25.4__tar.gz → 0.27.0__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 (189) hide show
  1. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/.gitignore +3 -1
  2. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/PKG-INFO +15 -4
  3. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/README.md +13 -3
  4. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/pyproject.toml +12 -2
  5. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_preparation.py +11 -0
  6. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/exec_directives.py +0 -6
  7. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/exec_runner.py +3 -28
  8. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/helpers.py +29 -2
  9. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/history_integration.py +27 -7
  10. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/runner.py +50 -16
  11. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/steps.py +12 -19
  12. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/default.md +21 -15
  13. tsugite_cli-0.27.0/tsugite/builtin_agents/job_reviewer.md +50 -0
  14. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/scheduling/SKILL.md +1 -1
  15. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/exec.py +1 -1
  16. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/run.py +6 -1
  17. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/usage.py +72 -4
  18. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/config.py +11 -3
  19. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/context_block.py +43 -4
  20. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/agent.py +118 -52
  21. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/record_store.py +4 -0
  22. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/sandbox.py +6 -7
  23. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/subprocess_executor.py +27 -43
  24. tsugite_cli-0.27.0/tsugite/egress_filter.py +66 -0
  25. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/models.py +1 -1
  26. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/sqlite_backend.py +13 -0
  27. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/hooks.py +27 -0
  28. tsugite_cli-0.27.0/tsugite/link_previews.py +79 -0
  29. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/md_agents.py +5 -0
  30. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/models.py +12 -0
  31. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/plugins.py +17 -9
  32. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/renderer.py +2 -2
  33. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/schemas/agent.schema.json +24 -0
  34. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/__init__.py +5 -2
  35. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/agents.py +1 -1
  36. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/daemon_control.py +4 -3
  37. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/history.py +27 -61
  38. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/http.py +7 -7
  39. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/jobs.py +62 -9
  40. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/schedule.py +13 -0
  41. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/secrets.py +8 -0
  42. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/sessions.py +4 -45
  43. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/skills.py +4 -4
  44. tsugite_cli-0.27.0/tsugite/tools/tool_access.py +154 -0
  45. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui_surfaces.py +47 -16
  46. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/usage/__init__.py +2 -0
  47. tsugite_cli-0.27.0/tsugite/usage/anomalies.py +85 -0
  48. tsugite_cli-0.27.0/tsugite/usage/cache.py +59 -0
  49. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/usage/store.py +34 -3
  50. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/user_agent.py +1 -1
  51. tsugite_cli-0.25.4/tsugite/core/proxy.py +0 -246
  52. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/AGENTS.md +0 -0
  53. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/CONTRIBUTING.md +0 -0
  54. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/LICENSE +0 -0
  55. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/__init__.py +0 -0
  56. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_inheritance.py +0 -0
  57. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/__init__.py +0 -0
  58. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/metrics.py +0 -0
  59. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/models.py +0 -0
  60. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_runner/validation.py +0 -0
  61. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/agent_utils.py +0 -0
  62. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/approval.py +0 -0
  63. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/__init__.py +0 -0
  64. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/agent_config.py +0 -0
  65. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/auto_context.py +0 -0
  66. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/base.py +0 -0
  67. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/delegation.py +0 -0
  68. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/file.py +0 -0
  69. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/inline.py +0 -0
  70. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/storage.py +0 -0
  71. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/attachments/url.py +0 -0
  72. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/.gitkeep +0 -0
  73. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/code_searcher.md +0 -0
  74. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/file_searcher.md +0 -0
  75. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/job_verifier.md +0 -0
  76. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/job_worker.md +0 -0
  77. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_agents/onboard.md +0 -0
  78. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_context.py +0 -0
  79. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/.gitkeep +0 -0
  80. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +0 -0
  81. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/external-agent-bridge/SKILL.md +0 -0
  82. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  83. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  84. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +0 -0
  85. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +0 -0
  86. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +0 -0
  87. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +0 -0
  88. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cache.py +0 -0
  89. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cancellation.py +0 -0
  90. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/__init__.py +0 -0
  91. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/agents.py +0 -0
  92. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/attachments.py +0 -0
  93. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/cache.py +0 -0
  94. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/chat.py +0 -0
  95. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/config.py +0 -0
  96. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/daemon.py +0 -0
  97. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/helpers.py +0 -0
  98. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/history.py +0 -0
  99. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/init.py +0 -0
  100. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/models.py +0 -0
  101. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/plugins.py +0 -0
  102. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/render.py +0 -0
  103. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/secrets.py +0 -0
  104. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/skills.py +0 -0
  105. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/tools.py +0 -0
  106. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/validate.py +0 -0
  107. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/cli/workspace.py +0 -0
  108. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/console.py +0 -0
  109. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/constants.py +0 -0
  110. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/context.py +0 -0
  111. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/__init__.py +0 -0
  112. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/content_blocks.py +0 -0
  113. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/executor.py +0 -0
  114. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/executor_registry.py +0 -0
  115. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/memory.py +0 -0
  116. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/state.py +0 -0
  117. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/core/tools.py +0 -0
  118. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/daemon_client.py +0 -0
  119. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/__init__.py +0 -0
  120. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/base.py +0 -0
  121. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/bus.py +0 -0
  122. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/events.py +0 -0
  123. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/events/helpers.py +0 -0
  124. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/exceptions.py +0 -0
  125. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/__init__.py +0 -0
  126. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/base.py +0 -0
  127. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/reconstruction.py +0 -0
  128. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/registry.py +0 -0
  129. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/sqlite_conn.py +0 -0
  130. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/sqlite_schema.py +0 -0
  131. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/history/ui_events.py +0 -0
  132. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/interaction.py +0 -0
  133. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/options.py +0 -0
  134. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/permissions.py +0 -0
  135. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/prompt_xml.py +0 -0
  136. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/__init__.py +0 -0
  137. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/anthropic.py +0 -0
  138. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/base.py +0 -0
  139. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/model_cache.py +0 -0
  140. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/model_registry.py +0 -0
  141. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/ollama.py +0 -0
  142. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/openai_compat.py +0 -0
  143. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/providers/openrouter.py +0 -0
  144. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/py.typed +0 -0
  145. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/schemas/__init__.py +0 -0
  146. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/__init__.py +0 -0
  147. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/backend.py +0 -0
  148. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/env.py +0 -0
  149. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/exec.py +0 -0
  150. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/file.py +0 -0
  151. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/masking.py +0 -0
  152. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/redaction.py +0 -0
  153. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/registry.py +0 -0
  154. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/secrets/sqlite.py +0 -0
  155. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/shell_tool_config.py +0 -0
  156. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/skill_discovery.py +0 -0
  157. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/AGENTS.md +0 -0
  158. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/IDENTITY.md +0 -0
  159. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/MEMORY.md +0 -0
  160. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/USER.md +0 -0
  161. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/personas/casual-technical.md +0 -0
  162. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/personas/marvin.md +0 -0
  163. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/templates/personas/minimal.md +0 -0
  164. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/artifacts.py +0 -0
  165. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/fs.py +0 -0
  166. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/interactive.py +0 -0
  167. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/notify.py +0 -0
  168. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/shell.py +0 -0
  169. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/shell_tools.py +0 -0
  170. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tools/time.py +0 -0
  171. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/tsugite.py +0 -0
  172. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/__init__.py +0 -0
  173. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/base.py +0 -0
  174. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/chat.py +0 -0
  175. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/dispatch.py +0 -0
  176. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/helpers.py +0 -0
  177. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/jsonl.py +0 -0
  178. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/plain.py +0 -0
  179. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_chat.py +0 -0
  180. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_commands.py +0 -0
  181. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_completer.py +0 -0
  182. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui/repl_handler.py +0 -0
  183. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/ui_context.py +0 -0
  184. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/usage/quota.py +0 -0
  185. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/utils.py +0 -0
  186. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/__init__.py +0 -0
  187. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/models.py +0 -0
  188. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/session.py +0 -0
  189. {tsugite_cli-0.25.4 → tsugite_cli-0.27.0}/tsugite/workspace/templates.py +0 -0
@@ -183,7 +183,6 @@ benchmarks/
183
183
  docker-compose.test.yml
184
184
  #### TODO ^^^
185
185
 
186
- plugins/tsugite-daemon/frontend/node_modules/
187
186
  # Vite dep-optimizer cache. Normally under node_modules/.vite, but lands at the
188
187
  # cwd's `.vite/` when vite/vitest is run from the repo root, so ignore it anywhere.
189
188
  .vite/
@@ -197,3 +196,6 @@ plugins/tsugite-daemon/tsugite_daemon/web/
197
196
  # Per-job git worktrees provisioned by the daemon.
198
197
  .tsugite-jobs/
199
198
  /packaging/runtime/.cache/
199
+ # Corporate CA certs dropped in for docker build.
200
+ /docker/ca-certificates/*
201
+ !/docker/ca-certificates/.gitkeep
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tsugite-cli
3
- Version: 0.25.4
3
+ Version: 0.27.0
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
@@ -261,6 +261,7 @@ Provides-Extra: all
261
261
  Requires-Dist: tsugite-acp; extra == 'all'
262
262
  Requires-Dist: tsugite-claude-code; extra == 'all'
263
263
  Requires-Dist: tsugite-codex-cli; extra == 'all'
264
+ Requires-Dist: tsugite-cursor; extra == 'all'
264
265
  Requires-Dist: tsugite-daemon; extra == 'all'
265
266
  Requires-Dist: tsugite-discord; extra == 'all'
266
267
  Requires-Dist: tsugite-pty; extra == 'all'
@@ -339,7 +340,7 @@ pip install tsugite-cli # or plain pip
339
340
  | --- | --- |
340
341
  | `tsugite-cli[daemon]` | Daemon: HTTP API, web UI, scheduler, jobs, terminals (pty), sandbox, Discord |
341
342
  | `tsugite-cli[web]` | `web_search` + YouTube transcripts |
342
- | `tsugite-cli[sandbox]` | bwrap sandbox backend (also needs the `bwrap` binary) |
343
+ | `tsugite-cli[sandbox]` | [Hozo](https://github.com/justyns/hozo) sandbox backend (needs `bwrap` on Linux; macOS uses the built-in `sandbox-exec`) |
343
344
  | `tsugite-cli[all]` | Everything above plus the optional tmux + provider plugins |
344
345
 
345
346
  ```bash
@@ -348,6 +349,16 @@ uv tool install "tsugite-cli[daemon]" # full server install
348
349
 
349
350
  The package is `tsugite-cli`, the command is `tsugite` (or `tsu` for short).
350
351
 
352
+ ### Desktop app
353
+
354
+ On Apple Silicon Macs (macOS 13 or later), install the desktop app from the Homebrew tap:
355
+
356
+ ```bash
357
+ brew install --cask justyns/tap/tsugite
358
+ ```
359
+
360
+ Linux builds ship as an AppImage on the [releases page](https://github.com/justyns/tsugite/releases).
361
+
351
362
  ## Quick start
352
363
 
353
364
  ```bash
@@ -384,7 +395,7 @@ in nested tmux / non-Rich-friendly shells. Pick a richer or quieter mode when yo
384
395
  - **Sub-agents** that can spawn other agents for specific subtasks
385
396
  - **Skills** directory-based knowledge modules (mostly) following the [agentskills.io](https://agentskills.io/) SKILL.md format
386
397
  - **Hooks** that fire shell commands on lifecycle events (post-tool, pre-message, pre/post-compact)
387
- - **Sandbox** (linux only) via bubblewrap with filesystem and network isolation
398
+ - **Sandbox** (Linux and macOS) via [Hozo](https://github.com/justyns/hozo) with filesystem and network isolation
388
399
 
389
400
  ## Agents in more detail
390
401
 
@@ -417,7 +428,7 @@ For a complete example, check the built-in [default agent](tsugite/builtin_agent
417
428
 
418
429
  ## Sandbox
419
430
 
420
- On Linux only (for now), agent code runs inside a [bubblewrap](https://github.com/containers/bubblewrap) sandbox when you pass `--sandbox`:
431
+ Agent code runs inside a [Hozo](https://github.com/justyns/hozo) sandbox (bubblewrap on Linux, `sandbox-exec` on macOS) when you pass `--sandbox`:
421
432
 
422
433
  ```bash
423
434
  tsu run +default "task" --sandbox --allow-domain "github.com"
@@ -55,7 +55,7 @@ pip install tsugite-cli # or plain pip
55
55
  | --- | --- |
56
56
  | `tsugite-cli[daemon]` | Daemon: HTTP API, web UI, scheduler, jobs, terminals (pty), sandbox, Discord |
57
57
  | `tsugite-cli[web]` | `web_search` + YouTube transcripts |
58
- | `tsugite-cli[sandbox]` | bwrap sandbox backend (also needs the `bwrap` binary) |
58
+ | `tsugite-cli[sandbox]` | [Hozo](https://github.com/justyns/hozo) sandbox backend (needs `bwrap` on Linux; macOS uses the built-in `sandbox-exec`) |
59
59
  | `tsugite-cli[all]` | Everything above plus the optional tmux + provider plugins |
60
60
 
61
61
  ```bash
@@ -64,6 +64,16 @@ uv tool install "tsugite-cli[daemon]" # full server install
64
64
 
65
65
  The package is `tsugite-cli`, the command is `tsugite` (or `tsu` for short).
66
66
 
67
+ ### Desktop app
68
+
69
+ On Apple Silicon Macs (macOS 13 or later), install the desktop app from the Homebrew tap:
70
+
71
+ ```bash
72
+ brew install --cask justyns/tap/tsugite
73
+ ```
74
+
75
+ Linux builds ship as an AppImage on the [releases page](https://github.com/justyns/tsugite/releases).
76
+
67
77
  ## Quick start
68
78
 
69
79
  ```bash
@@ -100,7 +110,7 @@ in nested tmux / non-Rich-friendly shells. Pick a richer or quieter mode when yo
100
110
  - **Sub-agents** that can spawn other agents for specific subtasks
101
111
  - **Skills** directory-based knowledge modules (mostly) following the [agentskills.io](https://agentskills.io/) SKILL.md format
102
112
  - **Hooks** that fire shell commands on lifecycle events (post-tool, pre-message, pre/post-compact)
103
- - **Sandbox** (linux only) via bubblewrap with filesystem and network isolation
113
+ - **Sandbox** (Linux and macOS) via [Hozo](https://github.com/justyns/hozo) with filesystem and network isolation
104
114
 
105
115
  ## Agents in more detail
106
116
 
@@ -133,7 +143,7 @@ For a complete example, check the built-in [default agent](tsugite/builtin_agent
133
143
 
134
144
  ## Sandbox
135
145
 
136
- On Linux only (for now), agent code runs inside a [bubblewrap](https://github.com/containers/bubblewrap) sandbox when you pass `--sandbox`:
146
+ Agent code runs inside a [Hozo](https://github.com/justyns/hozo) sandbox (bubblewrap on Linux, `sandbox-exec` on macOS) when you pass `--sandbox`:
137
147
 
138
148
  ```bash
139
149
  tsu run +default "task" --sandbox --allow-domain "github.com"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.25.4"
3
+ version = "0.27.0"
4
4
  description = "Micro-agent runner for task automation using markdown definitions"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
@@ -55,6 +55,7 @@ all = [
55
55
  "tsugite-acp",
56
56
  "tsugite-claude-code",
57
57
  "tsugite-codex-cli",
58
+ "tsugite-cursor",
58
59
  ]
59
60
 
60
61
  [project.scripts]
@@ -87,6 +88,7 @@ tsugite-tmux = { workspace = true }
87
88
  tsugite-acp = { workspace = true }
88
89
  tsugite-claude-code = { workspace = true }
89
90
  tsugite-codex-cli = { workspace = true }
91
+ tsugite-cursor = { workspace = true }
90
92
  tsugite-web = { workspace = true }
91
93
  tsugite-youtube = { workspace = true }
92
94
  tsugite-sandbox = { workspace = true }
@@ -95,6 +97,8 @@ tsugite-daemon = { workspace = true }
95
97
  tsugite-discord = { workspace = true }
96
98
  tsugite-cc-driver = { workspace = true }
97
99
  tsugite-onlyoffice = { workspace = true }
100
+ tsugite-mcp = { workspace = true }
101
+ tsugite-forge = { workspace = true }
98
102
 
99
103
  [tool.ruff]
100
104
  target-version = "py311"
@@ -170,8 +174,11 @@ paths = [
170
174
  "plugins/tsugite-cc-driver/tsugite_cc_driver",
171
175
  "plugins/tsugite-claude-code/tsugite_claude_code",
172
176
  "plugins/tsugite-codex-cli/tsugite_codex_cli",
177
+ "plugins/tsugite-cursor/tsugite_cursor",
173
178
  "plugins/tsugite-daemon/tsugite_daemon",
174
179
  "plugins/tsugite-discord/tsugite_discord",
180
+ "plugins/tsugite-forge/tsugite_forge",
181
+ "plugins/tsugite-mcp/tsugite_mcp",
175
182
  "plugins/tsugite-onlyoffice/tsugite_onlyoffice",
176
183
  "plugins/tsugite-pty/tsugite_pty",
177
184
  "plugins/tsugite-sandbox/tsugite_sandbox",
@@ -238,7 +245,7 @@ ignore_names = [
238
245
  "get_completions", # prompt_toolkit Completer API, dispatched by the session
239
246
  "complete_event", # mandatory prompt_toolkit get_completions() parameter
240
247
  "language", # CodeExecutionEvent field, read by the web UI via serialized events
241
- "BubblewrapSandbox", # plugin entry point (tsugite.sandbox: bwrap), loaded via get_sandbox_class
248
+ "HozoSandbox", # plugin entry point (tsugite.sandbox: hozo), loaded via get_sandbox_class
242
249
  # Public API / test-infra reached only from tests (vulture excludes tests).
243
250
  "subscribe_filtered", # imperative EventBus filtered-subscription API (docs/plugin-events.md)
244
251
  "unsubscribe_filtered",
@@ -264,6 +271,7 @@ dev = [
264
271
  "tsugite-acp",
265
272
  "tsugite-claude-code",
266
273
  "tsugite-codex-cli",
274
+ "tsugite-cursor",
267
275
  "tsugite-web",
268
276
  "tsugite-youtube",
269
277
  "tsugite-sandbox",
@@ -272,4 +280,6 @@ dev = [
272
280
  "tsugite-discord",
273
281
  "tsugite-cc-driver",
274
282
  "tsugite-onlyoffice",
283
+ "tsugite-mcp",
284
+ "tsugite-forge",
275
285
  ]
@@ -330,6 +330,17 @@ class AgentPreparer:
330
330
  if "notify_user" not in expanded and "notify_user" in _tools:
331
331
  expanded.append("notify_user")
332
332
 
333
+ if agent_config.deferred_tools:
334
+ from tsugite.permissions import get_permissions
335
+ from tsugite.tools.tool_access import TOOL_ACCESS_TOOLS, start_turn
336
+
337
+ perms = get_permissions()
338
+ granted = perms.listed(f"agents.{agent_config.name}.tools") if perms else []
339
+ deferred = expand_tool_specs(agent_config.deferred_tools) + granted
340
+ deferred = [t for t in deferred if t in _tools and (has_interaction or t not in interactive_tool_names)]
341
+ access = start_turn(agent_config.name, expanded, deferred)
342
+ expanded += [t for t in TOOL_ACCESS_TOOLS + access.loaded if t in _tools and t not in expanded]
343
+
333
344
  return [create_tool_from_tsugite(name) for name in expanded]
334
345
  except Exception as e:
335
346
  raise RuntimeError(f"Failed to create tools: {e}") from e
@@ -41,8 +41,6 @@ def _success_note(directive: ExecDirective) -> str:
41
41
  def execute_exec_directives(
42
42
  content: str,
43
43
  existing_context: Optional[Dict[str, Any]] = None,
44
- sandbox_config: Optional[Any] = None,
45
- workspace_dir: Optional[str] = None,
46
44
  event_bus: Optional[Any] = None,
47
45
  ) -> Tuple[str, Dict[str, Any]]:
48
46
  """Execute exec directives in `content` and return (modified_content, exec_context).
@@ -52,8 +50,6 @@ def execute_exec_directives(
52
50
  existing_context: Vars from prior pipeline stages (prefetch, tool directives,
53
51
  caller). Made available to each exec block as Python locals. Vars assigned
54
52
  by an earlier exec block in the same content are also visible to later ones.
55
- sandbox_config: Optional SandboxConfig; when set, blocks run inside bubblewrap.
56
- workspace_dir: Optional workspace path for sandbox bind mounts.
57
53
  event_bus: Optional EventBus for emitting warnings on continue_on_error paths.
58
54
 
59
55
  Returns:
@@ -89,8 +85,6 @@ def execute_exec_directives(
89
85
  locals_dict=block_locals,
90
86
  timeout=directive.timeout,
91
87
  continue_on_error=directive.continue_on_error,
92
- sandbox_config=sandbox_config,
93
- workspace_dir=workspace_dir,
94
88
  )
95
89
  except Exception as e:
96
90
  if not directive.continue_on_error:
@@ -4,15 +4,10 @@ This is a deliberate parallel path to `tsugite.core.subprocess_executor.Subproce
4
4
  Both run user Python in a subprocess, but they differ:
5
5
 
6
6
  - `SubprocessExecutor` is async, turn-stateful, blocks `open()`, forces tool usage,
7
- and manages a network proxy when sandboxed. It's designed for the LLM agent loop
8
- where each turn is a long-running session.
7
+ and runs inside the sandbox backend when sandboxed. It's designed for the LLM agent
8
+ loop where each turn is a long-running session.
9
9
  - `run_python_block` (this module) is synchronous, one-shot, allows plain Python
10
10
  (`open`, `os.listdir`, etc.), and is invoked at agent-prep time before the LLM runs.
11
-
12
- Sandbox plumbing is deferred: passing `sandbox_config` raises `NotImplementedError`.
13
- The async path's bubblewrap+proxy lifecycle does not translate cleanly to the
14
- synchronous one-shot model; a future shared `build_python_subprocess_command` helper
15
- would converge both paths. Any sandbox-related change must update both call sites.
16
11
  """
17
12
 
18
13
  import json
@@ -126,8 +121,6 @@ def run_python_block(
126
121
  locals_dict: Dict[str, Any],
127
122
  timeout: int = 30,
128
123
  continue_on_error: bool = False,
129
- sandbox_config: Optional[Any] = None,
130
- workspace_dir: Optional[str] = None,
131
124
  ) -> ExecBlockResult:
132
125
  """Run a Python block synchronously in a one-shot subprocess.
133
126
 
@@ -138,8 +131,6 @@ def run_python_block(
138
131
  timeout: Wall-clock timeout in seconds.
139
132
  continue_on_error: If True, exceptions are captured to result.error and
140
133
  return_value is None instead of re-raising.
141
- sandbox_config: Optional SandboxConfig; when set, blocks run in bwrap.
142
- workspace_dir: Optional workspace path for sandbox bind mounts.
143
134
 
144
135
  Raises:
145
136
  TimeoutError: On timeout.
@@ -158,7 +149,7 @@ def run_python_block(
158
149
  with open(payload_path, "w") as f:
159
150
  json.dump({"locals": safe_locals, "code": code}, f)
160
151
 
161
- cmd = _build_command(harness_path, payload_path, result_path, sandbox_config, workspace_dir)
152
+ cmd = [sys.executable, "-u", harness_path, payload_path, result_path]
162
153
 
163
154
  try:
164
155
  proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
@@ -205,19 +196,3 @@ def _filter_json_safe(d: Dict[str, Any]) -> Dict[str, Any]:
205
196
  except (TypeError, ValueError, OverflowError):
206
197
  continue
207
198
  return safe
208
-
209
-
210
- def _build_command(
211
- harness_path: str,
212
- payload_path: str,
213
- result_path: str,
214
- sandbox_config: Optional[Any],
215
- workspace_dir: Optional[str],
216
- ) -> list[str]:
217
- if sandbox_config is None:
218
- return [sys.executable, "-u", harness_path, payload_path, result_path]
219
- raise NotImplementedError(
220
- "sandbox_config support for tsu:exec is not implemented yet - the existing async "
221
- "SubprocessExecutor path manages a network proxy that doesn't translate cleanly to "
222
- "this synchronous one-shot path. Track in a follow-up."
223
- )
@@ -1,6 +1,7 @@
1
1
  """Shared helper functions for agent execution."""
2
2
 
3
3
  import contextvars
4
+ import uuid
4
5
  from dataclasses import dataclass, field
5
6
  from pathlib import Path
6
7
  from typing import TYPE_CHECKING, Any, List, Optional
@@ -105,7 +106,7 @@ def build_sandbox_policy(
105
106
  `tsu exec` so the two never drift. Agent frontmatter (network/sandbox) can only
106
107
  tighten the CLI/daemon ceiling, never loosen it.
107
108
 
108
- Raises RuntimeError if the sandbox is requested but bwrap is unavailable.
109
+ Raises RuntimeError if the sandbox is requested but no backend is available.
109
110
  """
110
111
  from tsugite.agent_runner.runner import resolve_effective_sandbox
111
112
  from tsugite.core.sandbox import SandboxConfig, sandbox_available
@@ -121,7 +122,9 @@ def build_sandbox_policy(
121
122
  return None, None
122
123
 
123
124
  if not sandbox_available():
124
- raise RuntimeError("No sandbox backend available. Install tsugite-sandbox and bubblewrap, or use --no-sandbox.")
125
+ raise RuntimeError(
126
+ "No sandbox backend available. Install tsugite-sandbox (and bubblewrap on Linux), or use --no-sandbox."
127
+ )
125
128
 
126
129
  ctx = SandboxContext(
127
130
  allow_domains=allow_domains,
@@ -197,6 +200,30 @@ def get_allowed_secrets() -> Optional[List[str]]:
197
200
  return _allowed_secrets_var.get()
198
201
 
199
202
 
203
+ _turn_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("turn_id", default=None)
204
+
205
+
206
+ def start_turn() -> contextvars.Token:
207
+ """Mint a fresh turn id for the history events recorded in this context. Returns a reset token."""
208
+ return _turn_id_var.set(uuid.uuid4().hex)
209
+
210
+
211
+ def reset_turn(token: contextvars.Token) -> None:
212
+ """Restore whatever turn id was current before the matching start_turn."""
213
+ _turn_id_var.reset(token)
214
+
215
+
216
+ def get_turn_id() -> Optional[str]:
217
+ """Return the current turn's id, or None outside a turn."""
218
+ return _turn_id_var.get()
219
+
220
+
221
+ def turn_fields() -> dict:
222
+ """The turn_id field for an event recorded now, or nothing outside a turn."""
223
+ turn_id = _turn_id_var.get()
224
+ return {"turn_id": turn_id} if turn_id else {}
225
+
226
+
200
227
  def clear_current_agent() -> None:
201
228
  """Clear the currently executing agent (tests and top-level teardown)."""
202
229
  _current_agent_var.set(None)
@@ -13,6 +13,7 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any, Dict, List, Optional
15
15
 
16
+ from tsugite.agent_runner.helpers import get_turn_id, turn_fields
16
17
  from tsugite.attachments.base import Attachment
17
18
  from tsugite.config import load_config
18
19
  from tsugite.history import (
@@ -88,6 +89,14 @@ def open_or_create_session(
88
89
  return backend.create(agent_name=agent_name, model=model, workspace=workspace)
89
90
 
90
91
 
92
+ def _turn_has(storage: Session, event_type: str) -> Optional[bool]:
93
+ """Whether the current turn recorded an event_type; None outside a turn."""
94
+ turn_id = get_turn_id()
95
+ if turn_id is None:
96
+ return None
97
+ return any(e.data.get("turn_id") == turn_id for e in storage.iter_events(types=(event_type,)))
98
+
99
+
91
100
  def _current_turn_already_has_user_input(storage: Session) -> bool:
92
101
  """True if the in-progress turn already recorded a user_input.
93
102
 
@@ -98,7 +107,13 @@ def _current_turn_already_has_user_input(storage: Session) -> bool:
98
107
  A genuinely repeated message opens a new turn *after* the prior session_end,
99
108
  so this stays False for it. A final_result also ends a turn copied without
100
109
  its session_end (branches, older compacted sessions).
110
+
111
+ That event-order rule covers callers with no turn id. With one set, only a
112
+ user_input carrying it counts.
101
113
  """
114
+ has = _turn_has(storage, "user_input")
115
+ if has is not None:
116
+ return has
102
117
  events = list(storage.iter_events(types=("user_input", "session_end", "final_result")))
103
118
  return bool(events) and events[-1].type == "user_input"
104
119
 
@@ -122,7 +137,7 @@ def record_user_input(
122
137
  """
123
138
  if _current_turn_already_has_user_input(storage):
124
139
  return
125
- data: Dict[str, Any] = {"text": text}
140
+ data: Dict[str, Any] = {"text": text, **turn_fields()}
126
141
  # Only files the user actually uploaded ride here: this field is display-only
127
142
  # (the web UI renders them as clickable `uploads/<name>` chips). The agent's
128
143
  # auto-included context (workspace memory files like USER.md, config
@@ -158,11 +173,15 @@ def record_final_result(
158
173
  the agent reaches its end-of-run recording). Skip in that case so the
159
174
  conversation view doesn't render the answer twice.
160
175
  """
161
- for event in reversed(list(storage.iter_events(types=("user_input", "final_result")))):
162
- if event.type == "user_input":
163
- break
164
- if event.type == "final_result":
165
- return
176
+ has = _turn_has(storage, "final_result")
177
+ if has:
178
+ return
179
+ if has is None:
180
+ for event in reversed(list(storage.iter_events(types=("user_input", "final_result")))):
181
+ if event.type == "user_input":
182
+ break
183
+ if event.type == "final_result":
184
+ return
166
185
  storage.record(
167
186
  "final_result",
168
187
  result=result,
@@ -170,6 +189,7 @@ def record_final_result(
170
189
  turns=turns,
171
190
  tokens=tokens,
172
191
  cost=cost,
192
+ **turn_fields(),
173
193
  )
174
194
 
175
195
 
@@ -189,7 +209,7 @@ def record_session_end(
189
209
  liveness. The two stores tracking different things (last-turn outcome vs
190
210
  conversation open/closed) is expected, not a desync.
191
211
  """
192
- storage.record("session_end", status=status, error_message=error_message)
212
+ storage.record("session_end", status=status, error_message=error_message, **turn_fields())
193
213
 
194
214
 
195
215
  def save_run_to_history(
@@ -12,7 +12,6 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional # noqa: E402
12
12
 
13
13
  from tsugite.core.agent import NO_ACTION_MESSAGE, AgentResult, TsugiteAgent # noqa: E402
14
14
  from tsugite.core.executor_registry import get_executor_class # noqa: E402
15
- from tsugite.core.proxy import _parse_pattern # noqa: E402
16
15
  from tsugite.core.state import session_state_path # noqa: E402
17
16
  from tsugite.exceptions import ( # noqa: E402
18
17
  AgentExecutionError,
@@ -29,13 +28,16 @@ from .helpers import ( # noqa: E402
29
28
  _stderr_console,
30
29
  build_sandbox_policy,
31
30
  get_display_console,
31
+ get_turn_id,
32
32
  get_ui_handler,
33
33
  reset_allowed_secrets,
34
34
  reset_current_agent,
35
35
  reset_sandbox_context,
36
+ reset_turn,
36
37
  set_allowed_secrets,
37
38
  set_current_agent,
38
39
  set_sandbox_context,
40
+ start_turn,
39
41
  )
40
42
  from .models import AgentExecutionResult, AgentSkippedError # noqa: E402
41
43
 
@@ -497,7 +499,7 @@ async def _execute_agent_with_prompt(
497
499
  )
498
500
 
499
501
  if sandbox_config is not None:
500
- # bwrap sandboxing is a subprocess feature, so a sandboxed run always uses
502
+ # Sandboxing is a subprocess feature, so a sandboxed run always uses
501
503
  # the subprocess executor regardless of the configured default backend.
502
504
  from tsugite.core.subprocess_executor import SubprocessExecutor
503
505
 
@@ -582,6 +584,10 @@ async def _execute_agent_with_prompt(
582
584
  pre_llm_call=pre_llm_call_cb,
583
585
  expects_action=requires_action or (channel_metadata or {}).get("source") == "scheduler",
584
586
  )
587
+ if agent_config.deferred_tools:
588
+ from tsugite.tools.tool_access import get_access
589
+
590
+ get_access().agent = agent
585
591
  # Set event_bus in context so tools can access it during execution
586
592
  _setup_event_context(event_bus)
587
593
 
@@ -609,13 +615,14 @@ async def _execute_agent_with_prompt(
609
615
  )
610
616
 
611
617
  try:
618
+ from tsugite.models import resolve_usage_model
612
619
  from tsugite.usage import get_usage_store
613
620
 
614
621
  meta = channel_metadata or {}
615
622
  get_usage_store().record(
616
623
  session_id=continue_conversation_id,
617
624
  agent=prepared.agent_config.name,
618
- model=model_string,
625
+ model=resolve_usage_model(model_string),
619
626
  source=meta.get("source") or "cli",
620
627
  schedule_name=meta.get("schedule_id"),
621
628
  input_tokens=agent.prompt_tokens,
@@ -750,8 +757,8 @@ def resolve_effective_sandbox(
750
757
  desired = set(fm_network.get("domains") or []) | set(fm_sandbox.get("allow_domains") or [])
751
758
  base = list(daemon_domains or [])
752
759
  if desired:
753
- # Keep each desired pattern only if the daemon ceiling permits it, using the
754
- # proxy's glob semantics (so agent "api.github.com" is kept under daemon
760
+ # Keep each desired pattern only if the daemon ceiling permits it, using
761
+ # glob semantics (so agent "api.github.com" is kept under daemon
755
762
  # "*.github.com"). An empty ceiling permits everything.
756
763
  effective = sorted(d for d in desired if _domain_within_ceiling(d, base))
757
764
  if not effective:
@@ -764,10 +771,25 @@ def resolve_effective_sandbox(
764
771
  return enabled, allow_domains, no_network
765
772
 
766
773
 
774
+ def _parse_pattern(pattern: str) -> tuple[str, set[int]]:
775
+ """No port means {80, 443} and `:*` means any port (empty set). IPv6 takes a port
776
+ only in brackets (`[fd00::/8]:443`), as Hozo's proxy does."""
777
+ if pattern.startswith("["):
778
+ domain, _, port_str = pattern[1:].partition("]")
779
+ port_str = port_str.removeprefix(":")
780
+ elif pattern.count(":") == 1:
781
+ domain, port_str = pattern.split(":")
782
+ else:
783
+ domain, port_str = pattern, ""
784
+ if not port_str:
785
+ return (domain, {80, 443})
786
+ return (domain, set() if port_str == "*" else {int(port_str)})
787
+
788
+
767
789
  def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
768
790
  """True if a desired domain:port pattern is permitted by the daemon ceiling.
769
791
 
770
- Uses the proxy's glob + port semantics so e.g. 'api.github.com' is within
792
+ Uses glob + port semantics so e.g. 'api.github.com' is within
771
793
  '*.github.com', but 'github.com:22' is NOT within 'github.com' (which only allows
772
794
  the default 80/443). The ceiling is a union: the desired ports must be covered by
773
795
  the union of port sets from ALL ceiling patterns whose domain glob matches (so
@@ -777,9 +799,6 @@ def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
777
799
  if not ceiling:
778
800
  return True
779
801
  d_domain, d_ports = _parse_pattern(desired_pattern.lower())
780
- # The proxy treats the allowlist as a union, so collect the ports from EVERY
781
- # ceiling pattern whose domain glob matches and check the desired ports against
782
- # their union (e.g. ["github.com:80","github.com:443"] together cover 80/443).
783
802
  matched = False
784
803
  allowed_ports: set = set()
785
804
  for c in ceiling:
@@ -1082,7 +1101,21 @@ async def run_agent_async(
1082
1101
  if context is None:
1083
1102
  context = {}
1084
1103
 
1085
- from tsugite.hooks import fire_pre_message_hooks
1104
+ from tsugite.hooks import fire_pre_message_hooks, reset_skip_hooks, resolve_skip_hooks, set_skip_hooks
1105
+
1106
+ # Parse agent configuration (with inheritance resolution)
1107
+ try:
1108
+ agent = parse_agent_file(agent_path)
1109
+ agent_config = agent.config
1110
+ except Exception as e:
1111
+ raise ValueError(f"Failed to parse agent file: {e}")
1112
+
1113
+ skip_hooks_token = set_skip_hooks(
1114
+ {
1115
+ *resolve_skip_hooks(agent_config.hooks, agent_config.skip_hooks),
1116
+ *(channel_metadata or {}).get("skip_hooks", []),
1117
+ }
1118
+ )
1086
1119
 
1087
1120
  hooks_dir = workspace.path if workspace else (path_context.effective_cwd if path_context else Path.cwd())
1088
1121
  hook_message = context.pop("raw_message", prompt)
@@ -1160,17 +1193,14 @@ async def run_agent_async(
1160
1193
  # New conversation (e.g., fresh workspace session) - start with empty history
1161
1194
  pass
1162
1195
 
1163
- # Parse agent configuration (with inheritance resolution)
1164
- try:
1165
- agent = parse_agent_file(agent_path)
1166
- agent_config = agent.config
1167
- except Exception as e:
1168
- raise ValueError(f"Failed to parse agent file: {e}")
1196
+ from tsugite.tools.tool_access import current_access
1169
1197
 
1170
1198
  # Scope this run's ambient state with tokens: an agent hook nests a full run
1171
1199
  # inside this one and must restore what it found rather than clobber it.
1172
1200
  agent_token = set_current_agent(agent_config.name)
1173
1201
  secrets_token = set_allowed_secrets(agent_config.allowed_secrets)
1202
+ turn_token = start_turn() if get_turn_id() is None else None
1203
+ access_token = current_access.set(None)
1174
1204
 
1175
1205
  setup = _RunSetup(
1176
1206
  exec_options=exec_options,
@@ -1205,6 +1235,10 @@ async def run_agent_async(
1205
1235
  finally:
1206
1236
  reset_current_agent(agent_token)
1207
1237
  reset_allowed_secrets(secrets_token)
1238
+ reset_skip_hooks(skip_hooks_token)
1239
+ current_access.reset(access_token)
1240
+ if turn_token is not None:
1241
+ reset_turn(turn_token)
1208
1242
 
1209
1243
 
1210
1244
  def preview_multistep_agent(