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