tsugite-cli 0.15.0__tar.gz → 0.16.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 (226) hide show
  1. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/AGENTS.md +14 -0
  2. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/PKG-INFO +3 -1
  3. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/README.md +2 -0
  4. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/pyproject.toml +1 -1
  5. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/__init__.py +12 -0
  6. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/helpers.py +116 -1
  7. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/runner.py +113 -14
  8. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +1 -0
  9. tsugite_cli-0.16.0/tsugite/builtin_skills/external-agent-bridge/SKILL.md +73 -0
  10. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/scheduling/SKILL.md +1 -0
  11. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +1 -0
  12. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +1 -0
  13. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +1 -0
  14. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +4 -2
  15. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/__init__.py +2 -0
  16. tsugite_cli-0.16.0/tsugite/cli/exec.py +176 -0
  17. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/__init__.py +2 -1
  18. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/agent.py +2 -4
  19. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/claude_code.py +15 -2
  20. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/executor.py +37 -2
  21. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/subprocess_executor.py +4 -1
  22. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/base.py +33 -1
  23. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/http.py +17 -7
  24. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/config.py +57 -1
  25. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/gateway.py +65 -0
  26. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/job_store.py +3 -0
  27. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/jobs_orchestrator.py +71 -14
  28. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/scheduler.py +59 -1
  29. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/session_runner.py +4 -0
  30. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/session_store.py +12 -1
  31. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/terminal_runtime.py +62 -0
  32. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/index.html +2 -2
  33. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/history.js +23 -9
  34. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/sessions.js +9 -0
  35. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversations.js +61 -18
  36. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/terminals.js +7 -1
  37. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/reconstruction.py +13 -0
  38. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/md_agents.py +4 -0
  39. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/options.py +2 -0
  40. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/anthropic.py +20 -5
  41. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/model_registry.py +10 -3
  42. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/openai_compat.py +4 -3
  43. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/schemas/agent.schema.json +13 -0
  44. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/__init__.py +29 -0
  45. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/agents.py +23 -5
  46. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/fs.py +16 -14
  47. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/jobs.py +6 -0
  48. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/schedule.py +6 -1
  49. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/sessions.py +20 -0
  50. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/skills.py +25 -7
  51. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/.gitignore +0 -0
  52. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/CONTRIBUTING.md +0 -0
  53. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/LICENSE +0 -0
  54. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/__init__.py +0 -0
  55. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_inheritance.py +0 -0
  56. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_preparation.py +0 -0
  57. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/exec_directives.py +0 -0
  58. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/exec_runner.py +0 -0
  59. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/history_integration.py +0 -0
  60. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/metrics.py +0 -0
  61. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/models.py +0 -0
  62. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/validation.py +0 -0
  63. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_utils.py +0 -0
  64. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/__init__.py +0 -0
  65. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/auto_context.py +0 -0
  66. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/base.py +0 -0
  67. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/file.py +0 -0
  68. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/inline.py +0 -0
  69. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/storage.py +0 -0
  70. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/url.py +0 -0
  71. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/youtube.py +0 -0
  72. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/.gitkeep +0 -0
  73. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/code_searcher.md +0 -0
  74. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/default.md +0 -0
  75. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/file_searcher.md +0 -0
  76. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/job_verifier.md +0 -0
  77. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/job_worker.md +0 -0
  78. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/onboard.md +0 -0
  79. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/.gitkeep +0 -0
  80. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
  81. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
  82. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cache.py +0 -0
  83. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/agents.py +0 -0
  84. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/attachments.py +0 -0
  85. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/cache.py +0 -0
  86. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/chat.py +0 -0
  87. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/config.py +0 -0
  88. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/daemon.py +0 -0
  89. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/helpers.py +0 -0
  90. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/history.py +0 -0
  91. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/init.py +0 -0
  92. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/models.py +0 -0
  93. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/plugins.py +0 -0
  94. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/render.py +0 -0
  95. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/run.py +0 -0
  96. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/secrets.py +0 -0
  97. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/skills.py +0 -0
  98. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/tools.py +0 -0
  99. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/usage.py +0 -0
  100. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/validate.py +0 -0
  101. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/workspace.py +0 -0
  102. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/config.py +0 -0
  103. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/console.py +0 -0
  104. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/constants.py +0 -0
  105. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/content_blocks.py +0 -0
  106. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/memory.py +0 -0
  107. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/proxy.py +0 -0
  108. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/sandbox.py +0 -0
  109. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/state.py +0 -0
  110. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/tools.py +0 -0
  111. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/__init__.py +0 -0
  112. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/__init__.py +0 -0
  113. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/discord.py +0 -0
  114. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/scheduler_adapter.py +0 -0
  115. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/auth.py +0 -0
  116. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/commands.py +0 -0
  117. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/compaction_scheduler.py +0 -0
  118. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/memory.py +0 -0
  119. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/pty_manager.py +0 -0
  120. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/push.py +0 -0
  121. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/record_store.py +0 -0
  122. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/terminal_store.py +0 -0
  123. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/console.css +0 -0
  124. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/job-flows.css +0 -0
  125. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/jobs-tab.css +0 -0
  126. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/jobs.css +0 -0
  127. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/responsive.css +0 -0
  128. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/styles.css +0 -0
  129. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/terminal.css +0 -0
  130. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/theme.css +0 -0
  131. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/tsu-modal.css +0 -0
  132. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/icon-192.png +0 -0
  133. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
  134. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/icon-512.png +0 -0
  135. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
  136. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
  137. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/api.js +0 -0
  138. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/app.js +0 -0
  139. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/utils/tsu-modal.js +0 -0
  140. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/utils/xterm-loader.js +0 -0
  141. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/utils.js +0 -0
  142. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/vendor/marked.LICENSE.md +0 -0
  143. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/vendor/marked.esm.min.js +0 -0
  144. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/attachments.js +0 -0
  145. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/event_types.js +0 -0
  146. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/input.js +0 -0
  147. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/streaming.js +0 -0
  148. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/file-editor.js +0 -0
  149. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/jobs.js +0 -0
  150. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/schedules.js +0 -0
  151. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/usage.js +0 -0
  152. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/webhooks.js +0 -0
  153. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/workspace.js +0 -0
  154. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/manifest.json +0 -0
  155. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/sw.js +0 -0
  156. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/webhook_store.py +0 -0
  157. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/__init__.py +0 -0
  158. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/base.py +0 -0
  159. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/bus.py +0 -0
  160. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/events.py +0 -0
  161. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/helpers.py +0 -0
  162. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/exceptions.py +0 -0
  163. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/__init__.py +0 -0
  164. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/models.py +0 -0
  165. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/storage.py +0 -0
  166. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/hooks.py +0 -0
  167. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/interaction.py +0 -0
  168. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/models.py +0 -0
  169. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/plugins.py +0 -0
  170. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/__init__.py +0 -0
  171. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/base.py +0 -0
  172. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/claude_code.py +0 -0
  173. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/model_cache.py +0 -0
  174. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/ollama.py +0 -0
  175. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/openrouter.py +0 -0
  176. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/renderer.py +0 -0
  177. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/schemas/__init__.py +0 -0
  178. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/__init__.py +0 -0
  179. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/backend.py +0 -0
  180. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/env.py +0 -0
  181. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/exec.py +0 -0
  182. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/file.py +0 -0
  183. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/masking.py +0 -0
  184. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/registry.py +0 -0
  185. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/sqlite.py +0 -0
  186. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/shell_tool_config.py +0 -0
  187. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/skill_discovery.py +0 -0
  188. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/AGENTS.md +0 -0
  189. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/IDENTITY.md +0 -0
  190. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/MEMORY.md +0 -0
  191. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/USER.md +0 -0
  192. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/personas/casual-technical.md +0 -0
  193. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/personas/marvin.md +0 -0
  194. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/personas/minimal.md +0 -0
  195. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/history.py +0 -0
  196. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/http.py +0 -0
  197. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/interactive.py +0 -0
  198. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/notify.py +0 -0
  199. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/scratchpad.py +0 -0
  200. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/secrets.py +0 -0
  201. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/shell.py +0 -0
  202. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/shell_tools.py +0 -0
  203. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/terminal.py +0 -0
  204. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/time.py +0 -0
  205. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tsugite.py +0 -0
  206. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/__init__.py +0 -0
  207. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/base.py +0 -0
  208. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/chat.py +0 -0
  209. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/helpers.py +0 -0
  210. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/jsonl.py +0 -0
  211. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/live.py +0 -0
  212. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/plain.py +0 -0
  213. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_chat.py +0 -0
  214. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_commands.py +0 -0
  215. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_completer.py +0 -0
  216. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_handler.py +0 -0
  217. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui_context.py +0 -0
  218. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/usage/__init__.py +0 -0
  219. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/usage/store.py +0 -0
  220. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/user_agent.py +0 -0
  221. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/utils.py +0 -0
  222. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/__init__.py +0 -0
  223. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/context.py +0 -0
  224. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/models.py +0 -0
  225. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/session.py +0 -0
  226. {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/templates.py +0 -0
@@ -44,6 +44,20 @@ uv run tsu render agent.md "task"
44
44
  uv run tsu validate agents/*.md
45
45
  ```
46
46
 
47
+ ### Reusing tsugite from external coding agents
48
+
49
+ `tsu exec` runs a Python snippet in tsugite's tool namespace, so a tsugite skill's code
50
+ (which calls `read_file`, `http_request`, `get_secret`, ...) runs without the full agent
51
+ loop. Secrets are allowlisted + masked; `--no-network` / `--sandbox` isolate the run. See
52
+ [docs/external-agent-integration.md](docs/external-agent-integration.md) for reusing skills
53
+ and agents from Claude Code, Cursor, or any agent that runs shell commands.
54
+
55
+ ```bash
56
+ echo 'read_file(path="README.md")' | tsu exec -
57
+ tsu exec snippet.py --tools @fs,@http --allow-secret gh-token
58
+ tsu run +<agent> "task" # or reuse a whole agent
59
+ ```
60
+
47
61
  ### Schema Management
48
62
  ```bash
49
63
  # Regenerate JSON schema after modifying AgentConfig
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsugite-cli
3
- Version: 0.15.0
3
+ Version: 0.16.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
@@ -400,6 +400,8 @@ tsu run +default "task" --sandbox --no-network
400
400
 
401
401
  Filesystem access is limited to the workspace. Network goes through a filtering proxy that only allows domains you specify.
402
402
 
403
+ The daemon can also sandbox its agents (off by default, configured in `daemon.yaml`). See [docs/sandbox.md](docs/sandbox.md).
404
+
403
405
  ## Config and Data Directories
404
406
 
405
407
  All paths follow [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/latest/) conventions and can be overridden with the standard environment variables.
@@ -129,6 +129,8 @@ tsu run +default "task" --sandbox --no-network
129
129
 
130
130
  Filesystem access is limited to the workspace. Network goes through a filtering proxy that only allows domains you specify.
131
131
 
132
+ The daemon can also sandbox its agents (off by default, configured in `daemon.yaml`). See [docs/sandbox.md](docs/sandbox.md).
133
+
132
134
  ## Config and Data Directories
133
135
 
134
136
  All paths follow [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/latest/) conventions and can be overridden with the standard environment variables.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tsugite-cli"
3
- version = "0.15.0"
3
+ version = "0.16.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"
@@ -3,16 +3,22 @@
3
3
  from tsugite.agent_runner.exec_directives import execute_exec_directives # noqa: F401
4
4
  from tsugite.agent_runner.exec_runner import ExecBlockResult, run_python_block # noqa: F401
5
5
  from tsugite.agent_runner.helpers import ( # noqa: F401
6
+ SandboxContext,
7
+ SandboxToolDeniedError,
6
8
  clear_allowed_agents,
7
9
  clear_current_agent,
10
+ clear_sandbox_context,
8
11
  get_allowed_agents,
9
12
  get_allowed_secrets,
10
13
  get_current_agent,
11
14
  get_display_console,
15
+ get_sandbox_context,
12
16
  get_ui_handler,
17
+ sandbox_context_to_override,
13
18
  set_allowed_agents,
14
19
  set_allowed_secrets,
15
20
  set_current_agent,
21
+ set_sandbox_context,
16
22
  )
17
23
  from tsugite.agent_runner.metrics import StepMetrics, display_step_metrics # noqa: F401
18
24
  from tsugite.agent_runner.models import AgentExecutionResult # noqa: F401
@@ -57,4 +63,10 @@ __all__ = [
57
63
  "set_allowed_secrets",
58
64
  "get_display_console",
59
65
  "get_ui_handler",
66
+ "SandboxContext",
67
+ "SandboxToolDeniedError",
68
+ "get_sandbox_context",
69
+ "set_sandbox_context",
70
+ "clear_sandbox_context",
71
+ "sandbox_context_to_override",
60
72
  ]
@@ -1,18 +1,133 @@
1
1
  """Shared helper functions for agent execution."""
2
2
 
3
3
  import threading
4
- from typing import Any, List, Optional
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, List, Optional
5
7
 
6
8
  from rich.console import Console
7
9
 
8
10
  from tsugite.console import get_stderr_console
9
11
 
12
+ if TYPE_CHECKING:
13
+ from tsugite.options import ExecutionOptions
14
+
10
15
  # Console for warnings and debug output (stderr)
11
16
  _stderr_console = get_stderr_console()
12
17
 
13
18
  # Thread-local storage for tracking currently executing agent
14
19
  _current_agent_context = threading.local()
15
20
 
21
+ # Thread-local storage for the active sandbox policy. Set per-run (in the same
22
+ # thread as the agent loop and its parent-only tool dispatch) when an agent runs
23
+ # sandboxed; read by host-exec/spawn tools to inherit the sandbox or be denied.
24
+ # Thread-local (like _current_agent_context) so concurrent daemon sessions don't
25
+ # clobber each other.
26
+ _sandbox_context = threading.local()
27
+
28
+
29
+ @dataclass
30
+ class SandboxContext:
31
+ """Effective sandbox policy for the currently executing agent.
32
+
33
+ Presence of a SandboxContext means the agent is running sandboxed; tools read
34
+ it to propagate the same isolation to anything they spawn.
35
+ """
36
+
37
+ allow_domains: List[str] = field(default_factory=list)
38
+ no_network: bool = False
39
+ extra_ro_binds: List[Path] = field(default_factory=list)
40
+ extra_rw_binds: List[Path] = field(default_factory=list)
41
+ workspace_dir: Optional[Path] = None
42
+
43
+
44
+ class SandboxToolDeniedError(RuntimeError):
45
+ """Raised when a host-exec tool is refused because the agent runs sandboxed
46
+ (see the deny_when_sandboxed decorator)."""
47
+
48
+
49
+ def set_sandbox_context(ctx: Optional["SandboxContext"]) -> None:
50
+ """Set (or clear, with None) the active sandbox policy for this thread."""
51
+ _sandbox_context.value = ctx
52
+
53
+
54
+ def get_sandbox_context() -> Optional["SandboxContext"]:
55
+ """Return the active sandbox policy, or None when not running sandboxed."""
56
+ return getattr(_sandbox_context, "value", None)
57
+
58
+
59
+ def clear_sandbox_context() -> None:
60
+ """Clear the active sandbox policy from thread-local storage."""
61
+ if hasattr(_sandbox_context, "value"):
62
+ delattr(_sandbox_context, "value")
63
+
64
+
65
+ def sandbox_context_to_override() -> Optional[dict]:
66
+ """Serialize the active sandbox policy as a metadata override dict, or None.
67
+
68
+ Spawn tools stamp this onto the records they create (sessions, jobs,
69
+ schedules) so the spawned daemon run inherits the same sandbox when it later
70
+ reaches the adapter chokepoint. The shape matches SandboxSettings so it can
71
+ be validated back there; paths are stringified to survive JSON metadata.
72
+ """
73
+ ctx = get_sandbox_context()
74
+ if ctx is None:
75
+ return None
76
+ return {
77
+ "enabled": True,
78
+ "no_network": ctx.no_network,
79
+ "allow_domains": list(ctx.allow_domains),
80
+ "extra_ro_binds": [str(p) for p in ctx.extra_ro_binds],
81
+ "extra_rw_binds": [str(p) for p in ctx.extra_rw_binds],
82
+ }
83
+
84
+
85
+ def build_sandbox_policy(
86
+ exec_options: "ExecutionOptions",
87
+ *,
88
+ workspace_dir: Optional[Path] = None,
89
+ agent_config: Any = None,
90
+ ):
91
+ """Resolve the effective sandbox policy into (SandboxConfig, SandboxContext).
92
+
93
+ Returns (None, None) when the sandbox is off. Shared by the agent runner and
94
+ `tsu exec` so the two never drift. Agent frontmatter (network/sandbox) can only
95
+ tighten the CLI/daemon ceiling, never loosen it.
96
+
97
+ Raises RuntimeError if the sandbox is requested but bwrap is unavailable.
98
+ """
99
+ from tsugite.agent_runner.runner import resolve_effective_sandbox
100
+ from tsugite.core.sandbox import BubblewrapSandbox, SandboxConfig
101
+
102
+ sandbox_on, allow_domains, no_network = resolve_effective_sandbox(
103
+ daemon_enabled=exec_options.sandbox,
104
+ daemon_domains=list(exec_options.allow_domains),
105
+ daemon_no_network=exec_options.no_network,
106
+ fm_network=getattr(agent_config, "network", None),
107
+ fm_sandbox=getattr(agent_config, "sandbox", None),
108
+ )
109
+ if not sandbox_on:
110
+ return None, None
111
+
112
+ if not BubblewrapSandbox.check_available():
113
+ raise RuntimeError("bwrap not found. Install bubblewrap or use --no-sandbox.")
114
+
115
+ ctx = SandboxContext(
116
+ allow_domains=allow_domains,
117
+ no_network=no_network,
118
+ extra_ro_binds=list(exec_options.extra_ro_binds),
119
+ extra_rw_binds=list(exec_options.extra_rw_binds),
120
+ workspace_dir=workspace_dir,
121
+ )
122
+ config = SandboxConfig(
123
+ allowed_domains=ctx.allow_domains,
124
+ no_network=ctx.no_network,
125
+ extra_ro_binds=ctx.extra_ro_binds,
126
+ extra_rw_binds=ctx.extra_rw_binds,
127
+ )
128
+ return config, ctx
129
+
130
+
16
131
  # Module-level storage for allowed agents (single-threaded CLI execution)
17
132
  # Subagents run in separate processes, so this doesn't need to be thread-local
18
133
  _allowed_agents: Optional[List[str]] = None
@@ -1,6 +1,7 @@
1
1
  """Agent execution engine using TsugiteAgent."""
2
2
 
3
3
  import asyncio
4
+ import fnmatch
4
5
  import logging
5
6
  import time
6
7
 
@@ -12,6 +13,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional # noqa: E402
12
13
  from tsugite.config import get_xdg_data_path # noqa: E402
13
14
  from tsugite.core.agent import TsugiteAgent # noqa: E402
14
15
  from tsugite.core.executor import LocalExecutor # noqa: E402
16
+ from tsugite.core.proxy import _parse_pattern # noqa: E402
15
17
  from tsugite.exceptions import AgentExecutionError, is_prompt_too_long_error # noqa: E402
16
18
  from tsugite.md_agents import AgentConfig, parse_agent_file # noqa: E402
17
19
  from tsugite.models import resolve_effective_model, strip_reserved_model_kwargs # noqa: E402
@@ -21,15 +23,18 @@ from tsugite.utils import is_interactive # noqa: E402
21
23
 
22
24
  from .helpers import ( # noqa: E402
23
25
  _stderr_console,
26
+ build_sandbox_policy,
24
27
  clear_allowed_agents,
25
28
  clear_current_agent,
26
29
  clear_multistep_ui_context,
30
+ clear_sandbox_context,
27
31
  get_display_console,
28
32
  get_ui_handler,
29
33
  print_step_progress,
30
34
  set_allowed_secrets,
31
35
  set_current_agent,
32
36
  set_multistep_ui_context,
37
+ set_sandbox_context,
33
38
  )
34
39
  from .metrics import StepMetrics, display_step_metrics # noqa: E402
35
40
  from .models import AgentExecutionResult # noqa: E402
@@ -527,25 +532,21 @@ async def _execute_agent_with_prompt(
527
532
  final_model_kwargs["reasoning_effort"] = resolved_effort
528
533
 
529
534
  # Create executor with workspace directory and event bus
530
- workspace_dir = workspace.path if workspace else None
535
+ workspace_dir = _resolve_workspace_dir(workspace, path_context)
531
536
 
532
537
  state_path = _resolve_state_path(continue_conversation_id)
533
538
 
534
- if exec_options.sandbox:
535
- from tsugite.core.sandbox import BubblewrapSandbox, SandboxConfig
536
- from tsugite.core.subprocess_executor import SubprocessExecutor
537
-
538
- if not BubblewrapSandbox.check_available():
539
- raise RuntimeError("bwrap not found. Install bubblewrap or use --no-sandbox.")
539
+ # The daemon config (exec_options) is the ceiling; the agent's frontmatter may
540
+ # only tighten it (opt in, force no_network, narrow domains), never loosen.
541
+ # build_sandbox_policy returns (None, None) when the sandbox is off; the same
542
+ # helper backs `tsu exec` so the two paths never drift.
543
+ sandbox_config, sandbox_ctx = build_sandbox_policy(
544
+ exec_options, workspace_dir=workspace_dir, agent_config=agent_config
545
+ )
540
546
 
541
- allowed_domains = list(exec_options.allow_domains)
542
- if agent_config.network:
543
- allowed_domains += agent_config.network.get("domains", [])
547
+ if sandbox_config is not None:
548
+ from tsugite.core.subprocess_executor import SubprocessExecutor
544
549
 
545
- sandbox_config = SandboxConfig(
546
- allowed_domains=allowed_domains,
547
- no_network=exec_options.no_network,
548
- )
549
550
  executor = SubprocessExecutor(
550
551
  workspace_dir=workspace_dir,
551
552
  event_bus=event_bus,
@@ -554,6 +555,8 @@ async def _execute_agent_with_prompt(
554
555
  state_path=state_path,
555
556
  session_id=continue_conversation_id,
556
557
  )
558
+ # Presence of the context == "this agent is sandboxed".
559
+ set_sandbox_context(sandbox_ctx)
557
560
  else:
558
561
  executor = LocalExecutor(
559
562
  workspace_dir=workspace_dir,
@@ -562,6 +565,7 @@ async def _execute_agent_with_prompt(
562
565
  state_path=state_path,
563
566
  session_id=continue_conversation_id,
564
567
  )
568
+ set_sandbox_context(None)
565
569
 
566
570
  # Inject variables into executor (for multi-step agents)
567
571
  if injectable_vars:
@@ -728,6 +732,11 @@ async def _execute_agent_with_prompt(
728
732
  else:
729
733
  raise RuntimeError(f"Agent execution failed: {e}")
730
734
  finally:
735
+ # Drop the sandbox policy from this thread so a later run on the same
736
+ # pooled thread (daemon asyncio.to_thread) can't read a stale context
737
+ # before it sets its own.
738
+ clear_sandbox_context()
739
+
731
740
  # Clean up subprocess executor temp files
732
741
  if hasattr(executor, "cleanup"):
733
742
  try:
@@ -755,6 +764,96 @@ async def _execute_agent_with_prompt(
755
764
  await asyncio.gather(*pending_tasks, return_exceptions=True)
756
765
 
757
766
 
767
+ def resolve_effective_sandbox(
768
+ *,
769
+ daemon_enabled: bool,
770
+ daemon_domains: list,
771
+ daemon_no_network: bool,
772
+ fm_network: Optional[dict],
773
+ fm_sandbox: Optional[dict],
774
+ ) -> tuple[bool, list, bool]:
775
+ """Combine the daemon sandbox policy with tighten-only frontmatter overrides.
776
+
777
+ The daemon config (or CLI flags) is the ceiling. An agent's frontmatter may make
778
+ itself MORE restricted - opt into the sandbox, force `no_network`, or narrow the
779
+ domain allowlist - but never less: it cannot disable the sandbox or reach a
780
+ domain the daemon didn't allow.
781
+
782
+ Returns (enabled, allow_domains, no_network).
783
+ """
784
+ fm_network = fm_network or {}
785
+ fm_sandbox = fm_sandbox or {}
786
+
787
+ enabled = bool(daemon_enabled) or bool(fm_sandbox.get("enabled"))
788
+ no_network = bool(daemon_no_network) or bool(fm_sandbox.get("no_network"))
789
+
790
+ # Domains the agent declares (network hints + an explicit cap list), capped to
791
+ # the daemon ceiling. An empty ceiling means "all", so the agent's declared set
792
+ # becomes the allowlist (narrowing from all to that set).
793
+ desired = set(fm_network.get("domains") or []) | set(fm_sandbox.get("allow_domains") or [])
794
+ base = list(daemon_domains or [])
795
+ if desired:
796
+ # Keep each desired pattern only if the daemon ceiling permits it, using the
797
+ # proxy's glob semantics (so agent "api.github.com" is kept under daemon
798
+ # "*.github.com"). An empty ceiling permits everything.
799
+ effective = sorted(d for d in desired if _domain_within_ceiling(d, base))
800
+ if not effective:
801
+ # The agent asked only for domains outside the ceiling -> grant none.
802
+ no_network = True
803
+ allow_domains = effective
804
+ else:
805
+ allow_domains = base
806
+
807
+ return enabled, allow_domains, no_network
808
+
809
+
810
+ def _domain_within_ceiling(desired_pattern: str, ceiling: list) -> bool:
811
+ """True if a desired domain:port pattern is permitted by the daemon ceiling.
812
+
813
+ Uses the proxy's glob + port semantics so e.g. 'api.github.com' is within
814
+ '*.github.com', but 'github.com:22' is NOT within 'github.com' (which only allows
815
+ the default 80/443). The ceiling is a union: the desired ports must be covered by
816
+ the union of port sets from ALL ceiling patterns whose domain glob matches (so
817
+ ['github.com:80','github.com:443'] together cover the default 80/443). An empty
818
+ port set means "all ports" (from '*:*'); an empty ceiling list means "all allowed".
819
+ """
820
+ if not ceiling:
821
+ return True
822
+ d_domain, d_ports = _parse_pattern(desired_pattern.lower())
823
+ # The proxy treats the allowlist as a union, so collect the ports from EVERY
824
+ # ceiling pattern whose domain glob matches and check the desired ports against
825
+ # their union (e.g. ["github.com:80","github.com:443"] together cover 80/443).
826
+ matched = False
827
+ allowed_ports: set = set()
828
+ for c in ceiling:
829
+ c_domain, c_ports = _parse_pattern(c.lower())
830
+ if not fnmatch.fnmatch(d_domain, c_domain):
831
+ continue
832
+ matched = True
833
+ if not c_ports: # this ceiling pattern allows all ports
834
+ return True
835
+ allowed_ports |= c_ports
836
+ if not matched:
837
+ return False
838
+ # Desired wanting all ports (empty set) needs an all-ports ceiling (handled above).
839
+ return bool(d_ports) and d_ports <= allowed_ports
840
+
841
+
842
+ def _resolve_workspace_dir(workspace: Optional[Any], path_context: Optional[Any]) -> Optional[Path]:
843
+ """Resolve the executor's workspace directory.
844
+
845
+ Prefer an explicit Workspace object (CLI workspace runs); otherwise fall back
846
+ to the PathContext's workspace_dir. The daemon passes only a path_context (no
847
+ Workspace), so without this fallback the sandbox would bind/chdir the daemon's
848
+ CWD instead of the agent's workspace (or job worktree).
849
+ """
850
+ if workspace:
851
+ return workspace.path
852
+ if path_context and getattr(path_context, "workspace_dir", None):
853
+ return path_context.workspace_dir
854
+ return None
855
+
856
+
758
857
  def run_agent(
759
858
  agent_path: Path,
760
859
  prompt: str,
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: codebase-exploration
3
3
  description: Strategies and patterns for exploring unfamiliar codebases efficiently, with adaptive approaches based on size and scope
4
+ jinja: false
4
5
  ---
5
6
 
6
7
  # Codebase Exploration Strategies
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: external-agent-bridge
3
+ description: How an external coding agent (Claude Code, Cursor, Aider, etc.) reuses tsugite agents and skills. Read before using any tsugite skill; their code runs in tsugite's tool namespace (read_file, http_request, get_secret) and must be run via `tsu exec`, not directly in a shell.
4
+ ---
5
+
6
+ # Reusing tsugite from an external coding agent
7
+
8
+ tsugite agents and skills run Python in tsugite's tool namespace: functions like
9
+ `read_file`, `http_request`, and `get_secret` are already in scope, no imports. To run that
10
+ code, use `tsu exec` (not a plain shell or REPL).
11
+
12
+ ## Using a tsugite skill
13
+
14
+ A skill is a directory with a `SKILL.md` (frontmatter + instructions) and optional
15
+ `scripts/`. To use one:
16
+
17
+ 1. Read its `SKILL.md` to see what it does and which tsugite tools its code calls. The
18
+ frontmatter may list `allowed-tools`.
19
+ 2. Look up the exact signatures before writing code, so you pass the right keyword args:
20
+ ```bash
21
+ tsu tools list # all tools, grouped by category
22
+ tsu tools show read_file # params for one tool, e.g. read_file(path=...)
23
+ ```
24
+ 3. Run the code with the tools it needs:
25
+ ```bash
26
+ tsu exec snippet.py --tools @fs,@http
27
+ echo 'print(read_file(path="README.md")[:200])' | tsu exec -
28
+ ```
29
+ If the skill ships a script, run it directly:
30
+ `tsu exec .claude/skills/<name>/scripts/foo.py --tools @fs`.
31
+
32
+ When writing the code:
33
+ - Call tools with keyword args: `read_file(path="x")`, not `read_file("x")`. Get the
34
+ param names from `tsu tools show <name>`.
35
+ - `open()` is blocked; use `read_file` / `write_file`.
36
+ - A trailing expression is printed, like `python -c`.
37
+
38
+ ## Tools
39
+
40
+ - `--tools` selects what is available: categories (`@fs`, `@http`, `@secrets`, `@shell`,
41
+ `@time`, ...) or bare names. Pass several by repeating the flag or comma-separating
42
+ (`--tools @fs,@http`). Default exposes `@fs`, `@http`, `@secrets`.
43
+ - `--agent +name` inherits that agent's exact tools and secret allowlist.
44
+ - `tsu tools list` / `tsu tools show <name>` are the source of truth for names and params.
45
+
46
+ ## Secrets
47
+
48
+ `get_secret(name="...")` returns the value, masked as `***` in output. Allowlist it:
49
+
50
+ ```bash
51
+ tsu exec snippet.py --allow-secret gh-token
52
+ tsu exec snippet.py --agent +deploy # use the agent's allowed_secrets
53
+ ```
54
+
55
+ No allowlist means all secrets are allowed (still masked).
56
+
57
+ ## Sandbox
58
+
59
+ ```bash
60
+ tsu exec snippet.py --no-network # no network (implies --sandbox)
61
+ tsu exec snippet.py --sandbox --allow-domain api.github.com
62
+ ```
63
+
64
+ ## Whole agents
65
+
66
+ If a task matches an existing agent, run it instead of rebuilding it:
67
+
68
+ ```bash
69
+ tsu agents list
70
+ tsu run +<agent> "task"
71
+ ```
72
+
73
+ See `docs/external-agent-integration.md` in the tsugite repo for more.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: scheduling
3
3
  description: How to create, manage, and monitor scheduled agent tasks using Tsugite's schedule tools
4
+ jinja: false
4
5
  ---
5
6
 
6
7
  # Scheduling Agent Tasks
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: skill-authoring
3
3
  description: How to write effective skills, convert agentskills.io/Claude Code skills to tsugite format, and review skills for safety; load when authoring, converting, or auditing skills
4
+ jinja: false
4
5
  ---
5
6
 
6
7
  # Skill Authoring Guide
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: tsugite-agent-basics
3
3
  description: Overview of how Tsugite agents are defined, resolved, and executed; load when inspecting, authoring, or troubleshooting agents
4
+ jinja: false
4
5
  ---
5
6
 
6
7
  # Tsugite Agent Basics
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: tsugite-jinja-reference
3
3
  description: Reference for Jinja templating in Tsugite agents, including helpers and context variables; load when editing templates or debugging rendering
4
+ jinja: false
4
5
  ---
5
6
 
6
7
  # Tsugite Jinja Reference
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: tsugite-skill-basics
3
3
  description: How Tsugite skills are structured, discovered, and loaded at runtime; load when creating, auditing, or debugging skills
4
+ jinja: false
4
5
  ---
5
6
 
6
7
  # Tsugite Skill Basics
@@ -27,7 +28,7 @@ skill-name/
27
28
  assets/ # optional: templates, data, or other static resources
28
29
  ```
29
30
 
30
- `SKILL.md` itself is a Jinja2-rendered Markdown file:
31
+ `SKILL.md` itself is a Jinja2-rendered Markdown file (set `jinja: false` in frontmatter to opt out and show the body verbatim):
31
32
 
32
33
  ```yaml
33
34
  ---
@@ -54,6 +55,7 @@ Available in templates:
54
55
  **Tsugite extensions** (not part of the agentskills.io spec; safe to omit for cross-client portability):
55
56
  - `triggers: [keywords]` - when any listed keyword appears in the user prompt (word-boundary match, case-insensitive), the skill is auto-loaded.
56
57
  - `ttl: N` - sticky time-to-live (in user messages). Only applies in the daemon. A sticky skill that goes N turns without being referenced is auto-unloaded. `0` means never expire. Defaults to the global `skill_ttl_default` config value (currently 10).
58
+ - `jinja: false` - skip Jinja rendering for this skill's body, showing it verbatim. Use for documentation/reference skills whose content contains literal `{{ }}` / `{% %}` examples that must not be executed. Default is `true`. (`<!-- tsu:ignore -->` blocks are still stripped either way.)
57
59
 
58
60
  ## Discovery Order
59
61
 
@@ -236,7 +238,7 @@ read_file(results[0])
236
238
 
237
239
  ### Undefined Variable in Template
238
240
 
239
- Skills only have `today()`, `now()`, `env`, and `user_prompt`. Remove references to other variables.
241
+ Skills only have `today()`, `now()`, `env`, and `user_prompt`. Remove references to other variables, or set `jinja: false` in frontmatter if the `{{ }}` are literal documentation examples that should not be rendered at all.
240
242
 
241
243
  ### Name/Directory Mismatch Warning
242
244
 
@@ -27,12 +27,14 @@ def version():
27
27
 
28
28
  # Register main commands from split modules
29
29
  from .chat import chat # noqa: E402
30
+ from .exec import exec_cmd # noqa: E402
30
31
  from .render import render # noqa: E402
31
32
  from .run import run # noqa: E402
32
33
 
33
34
  app.command()(run)
34
35
  app.command()(render)
35
36
  app.command()(chat)
37
+ app.command("exec")(exec_cmd)
36
38
 
37
39
  # Backward-compatible re-exports + subcommand registration
38
40
  from .agents import agents_app # noqa: E402