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.
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/AGENTS.md +14 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/PKG-INFO +3 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/README.md +2 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/pyproject.toml +1 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/__init__.py +12 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/helpers.py +116 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/runner.py +113 -14
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/codebase-exploration/SKILL.md +1 -0
- tsugite_cli-0.16.0/tsugite/builtin_skills/external-agent-bridge/SKILL.md +73 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/scheduling/SKILL.md +1 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/skill-authoring/SKILL.md +1 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-agent-basics/SKILL.md +1 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-jinja-reference/SKILL.md +1 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md +4 -2
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/__init__.py +2 -0
- tsugite_cli-0.16.0/tsugite/cli/exec.py +176 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/__init__.py +2 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/agent.py +2 -4
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/claude_code.py +15 -2
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/executor.py +37 -2
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/subprocess_executor.py +4 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/base.py +33 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/http.py +17 -7
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/config.py +57 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/gateway.py +65 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/job_store.py +3 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/jobs_orchestrator.py +71 -14
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/scheduler.py +59 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/session_runner.py +4 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/session_store.py +12 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/terminal_runtime.py +62 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/index.html +2 -2
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/history.js +23 -9
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/sessions.js +9 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversations.js +61 -18
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/terminals.js +7 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/reconstruction.py +13 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/md_agents.py +4 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/options.py +2 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/anthropic.py +20 -5
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/model_registry.py +10 -3
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/openai_compat.py +4 -3
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/schemas/agent.schema.json +13 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/__init__.py +29 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/agents.py +23 -5
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/fs.py +16 -14
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/jobs.py +6 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/schedule.py +6 -1
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/sessions.py +20 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/skills.py +25 -7
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/.gitignore +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/CONTRIBUTING.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/LICENSE +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_inheritance.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_preparation.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/exec_directives.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/exec_runner.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/history_integration.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/metrics.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/models.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_runner/validation.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/agent_utils.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/auto_context.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/base.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/file.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/inline.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/storage.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/url.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/attachments/youtube.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/.gitkeep +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/code_searcher.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/default.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/file_searcher.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/job_verifier.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/job_worker.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_agents/onboard.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/.gitkeep +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/python-math/SKILL.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/response-patterns/SKILL.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cache.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/agents.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/attachments.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/cache.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/chat.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/config.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/daemon.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/helpers.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/history.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/init.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/models.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/plugins.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/render.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/run.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/secrets.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/skills.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/tools.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/usage.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/validate.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/cli/workspace.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/config.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/console.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/constants.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/content_blocks.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/memory.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/proxy.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/sandbox.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/state.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/core/tools.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/discord.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/adapters/scheduler_adapter.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/auth.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/commands.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/compaction_scheduler.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/memory.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/pty_manager.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/push.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/record_store.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/terminal_store.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/console.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/job-flows.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/jobs-tab.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/jobs.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/responsive.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/styles.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/terminal.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/theme.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/css/tsu-modal.css +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/icon-192.png +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/icon-512-maskable.png +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/icon-512.png +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/screenshot-narrow.png +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/icons/screenshot-wide.png +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/api.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/app.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/utils/tsu-modal.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/utils/xterm-loader.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/utils.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/vendor/marked.LICENSE.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/vendor/marked.esm.min.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/attachments.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/event_types.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/input.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/conversation/streaming.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/file-editor.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/jobs.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/schedules.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/usage.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/webhooks.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/js/views/workspace.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/manifest.json +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/web/sw.js +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/daemon/webhook_store.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/base.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/bus.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/events.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/events/helpers.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/exceptions.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/models.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/history/storage.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/hooks.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/interaction.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/models.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/plugins.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/base.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/claude_code.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/model_cache.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/ollama.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/providers/openrouter.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/renderer.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/schemas/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/backend.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/env.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/exec.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/file.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/masking.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/registry.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/secrets/sqlite.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/shell_tool_config.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/skill_discovery.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/AGENTS.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/IDENTITY.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/MEMORY.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/USER.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/personas/casual-technical.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/personas/marvin.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/templates/personas/minimal.md +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/history.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/http.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/interactive.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/notify.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/scratchpad.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/secrets.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/shell.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/shell_tools.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/terminal.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tools/time.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/tsugite.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/base.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/chat.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/helpers.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/jsonl.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/live.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/plain.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_chat.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_commands.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_completer.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui/repl_handler.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/ui_context.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/usage/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/usage/store.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/user_agent.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/utils.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/__init__.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/context.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/models.py +0 -0
- {tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/workspace/session.py +0 -0
- {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.
|
|
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.
|
|
@@ -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
|
|
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
|
|
535
|
+
workspace_dir = _resolve_workspace_dir(workspace, path_context)
|
|
531
536
|
|
|
532
537
|
state_path = _resolve_state_path(continue_conversation_id)
|
|
533
538
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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,
|
|
@@ -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.
|
{tsugite_cli-0.15.0 → tsugite_cli-0.16.0}/tsugite/builtin_skills/tsugite-skill-basics/SKILL.md
RENAMED
|
@@ -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
|